AutoFixture.AutoMoq为一个构造函数参数提供已知值

我刚开始在我的unit testing中使用AutoFixture.AutoMoq ,我发现它对于创建我不关心特定值的对象非常有帮助。 毕竟,匿名对象创建就是它的全部。

我正在努力的是当我关心一个或多个构造函数参数时。 以下面的ExampleComponent

 public class ExampleComponent { public ExampleComponent(IService service, string someValue) { } } 

我想编写一个测试,其中我为someValue提供了一个特定的值,但是由AutoFixture.AutoMoq自动创建IService

我知道如何在我的IFixture上使用Freeze来保持一个已注入组件的已知值,但我不太明白如何提供我自己的已知值。

这是我理想的做法:

 [TestMethod] public void Create_ExampleComponent_With_Known_SomeValue() { // create a fixture that supports automocking IFixture fixture = new Fixture().Customize(new AutoMoqCustomization()); // supply a known value for someValue (this method doesn't exist) string knownValue = fixture.Freeze("My known value"); // create an ExampleComponent with my known value injected // but without bothering about the IService parameter ExampleComponent component = this.fixture.Create(); // exercise component knowning it has my known value injected ... } 

我知道我可以通过直接调用构造函数来做到这一点,但这不再是匿名对象创建。 有没有办法像这样使用AutoFixture.AutoMock,还是我需要在我的测试中加入一个DI容器才能做我想做的事情?


编辑:

在我原来的问题中,我可能应该不那么抽搐,所以这是我的具体情况。

我有一个ICache接口,它具有通用的TryReadWrite方法:

 public interface ICache { bool TryRead(string key, out T value); void Write(string key, T value); // other methods not shown... } 

我正在实现一个CookieCache ,其中ITypeConverter处理将对象转换为字符串和从字符串转换对象, lifespan用于设置cookie的到期日期。

 public class CookieCache : ICache { public CookieCache(ITypeConverter converter, TimeSpan lifespan) { // usual storing of parameters } public bool TryRead(string key, out T result) { // read the cookie value as string and convert it to the target type } public void Write(string key, T value) { // write the value to a cookie, converted to a string // set the expiry date of the cookie using the lifespan } // other methods not shown... } 

因此,当为cookie的有效期编写测试时,我关心的是生命周期而不是转换器。

你必须替换:

 string knownValue = fixture.Freeze("My known value"); 

有:

 fixture.Inject("My known value"); 

您可以在此处阅读有关Inject更多信息。


实际上Freeze扩展方法可以:

 var value = fixture.Create(); fixture.Inject(value); return value; 

这意味着您在测试中使用的重载实际上称为带有种子的Create我的已知值导致“我的已知 4d41f94f-1fc9-4115-9f29-e50bc2b4ba5e”

所以我相信人们可以解决马克建议的普遍实施,但我想我会发表评论。

我已经基于Mark的LifeSpanArg创建了一个通用的ParameterNameSpecimenBuilder

 public class ParameterNameSpecimenBuilder : ISpecimenBuilder { private readonly string name; private readonly T value; public ParameterNameSpecimenBuilder(string name, T value) { // we don't want a null name but we might want a null value if (string.IsNullOrWhiteSpace(name)) { throw new ArgumentNullException("name"); } this.name = name; this.value = value; } public object Create(object request, ISpecimenContext context) { var pi = request as ParameterInfo; if (pi == null) { return new NoSpecimen(request); } if (pi.ParameterType != typeof(T) || !string.Equals( pi.Name, this.name, StringComparison.CurrentCultureIgnoreCase)) { return new NoSpecimen(request); } return this.value; } } 

然后我在FreezeByName上定义了一个通用的FreezeByName扩展方法,它设置了自定义:

 public static class FreezeByNameExtension { public static void FreezeByName(this IFixture fixture, string name, T value) { fixture.Customizations.Add(new ParameterNameSpecimenBuilder(name, value)); } } 

以下测试现在将通过:

 [TestMethod] public void FreezeByName_Sets_Value1_And_Value2_Independently() { //// Arrange IFixture arrangeFixture = new Fixture(); string myValue1 = arrangeFixture.Create(); string myValue2 = arrangeFixture.Create(); IFixture sutFixture = new Fixture(); sutFixture.FreezeByName("value1", myValue1); sutFixture.FreezeByName("value2", myValue2); //// Act TestClass result = sutFixture.Create>(); //// Assert Assert.AreEqual(myValue1, result.Value1); Assert.AreEqual(myValue2, result.Value2); } public class TestClass { public TestClass(T value1, T value2) { this.Value1 = value1; this.Value2 = value2; } public T Value1 { get; private set; } public T Value2 { get; private set; } } 

可以这样做。 想象一下,您希望为TimeSpan参数指定一个名为lifespan的特定值。

 public class LifespanArg : ISpecimenBuilder { private readonly TimeSpan lifespan; public LifespanArg(TimeSpan lifespan) { this.lifespan = lifespan; } public object Create(object request, ISpecimenContext context) { var pi = request as ParameterInfo; if (pi == null) return new NoSpecimen(request); if (pi.ParameterType != typeof(TimeSpan) || pi.Name != "lifespan") return new NoSpecimen(request); return this.lifespan; } } 

当然,它可以像这样使用:

 var fixture = new Fixture(); fixture.Customizations.Add(new LifespanArg(mySpecialLifespanValue)); var sut = fixture.Create(); 

这种方法可以在某种程度上推广,但最后,我们受限于缺乏一种强类型的方法来从特定的构造函数或方法参数中提取ParameterInfo。

我费用就像@Nick几乎就在那里。 覆盖构造函数参数时,它必须是给定类型,并且仅限于该类型。

首先,我们创建一个新的ISpecimenBuilder,它查看“Member.DeclaringType”以保持正确的范围。

 public class ConstructorArgumentRelay : ISpecimenBuilder { private readonly string _paramName; private readonly TValueType _value; public ConstructorArgumentRelay(string ParamName, TValueType value) { _paramName = ParamName; _value = value; } public object Create(object request, ISpecimenContext context) { if (context == null) throw new ArgumentNullException("context"); ParameterInfo parameter = request as ParameterInfo; if (parameter == null) return (object)new NoSpecimen(request); if (parameter.Member.DeclaringType != typeof(TTarget) || parameter.Member.MemberType != MemberTypes.Constructor || parameter.ParameterType != typeof(TValueType) || parameter.Name != _paramName) return (object)new NoSpecimen(request); return _value; } } 

接下来,我们创建一个扩展方法,以便我们可以使用AutoFixture轻松连接它。

 public static class AutoFixtureExtensions { public static IFixture ConstructorArgumentFor( this IFixture fixture, string paramName, TValueType value) { fixture.Customizations.Add( new ConstructorArgumentRelay(paramName, value) ); return fixture; } } 

现在我们创建两个类似的类来测试。

  public class TestClass { public TestClass(T value1, T value2) { Value1 = value1; Value2 = value2; } public T Value1 { get; private set; } public T Value2 { get; private set; } } public class SimilarClass { public SimilarClass(T value1, T value2) { Value1 = value1; Value2 = value2; } public T Value1 { get; private set; } public T Value2 { get; private set; } } 

最后,我们使用原始测试的扩展来测试它,看它不会覆盖类似命名和类型的构造函数参数。

 [TestFixture] public class AutoFixtureTests { [Test] public void Can_Create_Class_With_Specific_Parameter_Value() { string wanted = "This is the first string"; string wanted2 = "This is the second string"; Fixture fixture = new Fixture(); fixture.ConstructorArgumentFor, string>("value1", wanted) .ConstructorArgumentFor, string>("value2", wanted2); TestClass t = fixture.Create>(); SimilarClass s = fixture.Create>(); Assert.AreEqual(wanted,t.Value1); Assert.AreEqual(wanted2,t.Value2); Assert.AreNotEqual(wanted,s.Value1); Assert.AreNotEqual(wanted2,s.Value2); } } 

这似乎是这里设置最全面的解决方案。 所以我要添加我的:

创建可以处理多个构造函数参数的ISpecimenBuilder的第一件事

 internal sealed class CustomConstructorBuilder : ISpecimenBuilder { private readonly Dictionary _ctorParameters = new Dictionary(); public object Create(object request, ISpecimenContext context) { var type = typeof (T); var sr = request as SeededRequest; if (sr == null || !sr.Request.Equals(type)) { return new NoSpecimen(request); } var ctor = type.GetConstructors(BindingFlags.Instance | BindingFlags.Public).FirstOrDefault(); if (ctor == null) { return new NoSpecimen(request); } var values = new List(); foreach (var parameter in ctor.GetParameters()) { if (_ctorParameters.ContainsKey(parameter.Name)) { values.Add(_ctorParameters[parameter.Name]); } else { values.Add(context.Resolve(parameter.ParameterType)); } } return ctor.Invoke(BindingFlags.CreateInstance, null, values.ToArray(), CultureInfo.InvariantCulture); } public void Addparameter(string paramName, object val) { _ctorParameters.Add(paramName, val); } } 

然后创建扩展方法,简化创建的构建器的使用

  public static class AutoFixtureExtensions { public static void FreezeActivator(this IFixture fixture, object parameters) { var builder = new CustomConstructorBuilder(); foreach (var prop in parameters.GetType().GetProperties()) { builder.Addparameter(prop.Name, prop.GetValue(parameters)); } fixture.Customize(x => builder); } } 

用法:

 var f = new Fixture(); f.FreezeActivator(new { privateId = 15, parentId = (long?)33 });