30

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

当我关心一个或多个构造函数参数时,我正在苦苦挣扎。采取ExampleComponent以下措施:

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

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

我知道如何使用FreezeIFixture来保持将被注入组件的已知值,但我不太清楚如何提供我自己的已知值。

这是我最想做的事情:

[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<string>("My known value");

    // create an ExampleComponent with my known value injected 
    // but without bothering about the IService parameter
    ExampleComponent component = this.fixture.Create<ExampleComponent>();

    // exercise component knowning it has my known value injected
    ...
}

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


编辑:

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

我有一个ICache具有泛型TryRead<T>Write<T>方法的接口:

public interface ICache
{
    bool TryRead<T>(string key, out T value);

    void Write<T>(string key, T value);

    // other methods not shown...  
}

我正在实现一个CookieCachewhereITypeConverter处理对象与字符串之间的转换,并lifespan用于设置 cookie 的到期日期。

public class CookieCache : ICache
{
    public CookieCache(ITypeConverter converter, TimeSpan lifespan)
    {
        // usual storing of parameters
    }

    public bool TryRead<T>(string key, out T result)
    {
        // read the cookie value as string and convert it to the target type
    }

    public void Write<T>(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 的到期日期编写测试时,我关心的是寿命,而不是转换器。

4

6 回答 6

22

所以我相信人们可以制定出马克建议的普遍实施,但我想我会发布它以征求意见。

我创建了一个ParameterNameSpecimenBuilder基于 Mark 的泛型LifeSpanArg

public class ParameterNameSpecimenBuilder<T> : 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扩展方法,IFixture用于设置自定义:

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

现在将通过以下测试:

[TestMethod]
public void FreezeByName_Sets_Value1_And_Value2_Independently()
{
    //// Arrange
    IFixture arrangeFixture = new Fixture();

    string myValue1 = arrangeFixture.Create<string>();
    string myValue2 = arrangeFixture.Create<string>();

    IFixture sutFixture = new Fixture();
    sutFixture.FreezeByName("value1", myValue1);
    sutFixture.FreezeByName("value2", myValue2);

    //// Act
    TestClass<string> result = sutFixture.Create<TestClass<string>>();

    //// Assert
    Assert.AreEqual(myValue1, result.Value1);
    Assert.AreEqual(myValue2, result.Value2);
}

public class TestClass<T>
{
    public TestClass(T value1, T value2)
    {
        this.Value1 = value1;
        this.Value2 = value2;
    }

    public T Value1 { get; private set; }

    public T Value2 { get; private set; }
}
于 2013-06-06T08:28:49.747 回答
20

你必须更换:

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

和:

fixture.Inject("My known value");

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


实际上Freeze扩展方法是:

var value = fixture.Create<T>();
fixture.Inject(value);
return value;

这意味着您在测试中使用的重载实际上是Create<T>用种子调用的:My known value导致"My known value4d41f94f-1fc9-4115-9f29-e50bc2b4ba5e"

于 2013-05-29T17:31:48.737 回答
15

可以做这样的事情。想象一下,您想为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<CookieCache>();

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

于 2013-06-06T06:01:16.163 回答
11

我觉得@Nick 快到了。覆盖构造函数参数时,它需要用于给定类型,并且仅限于该类型。

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

public class ConstructorArgumentRelay<TTarget,TValueType> : 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<TTargetType, TValueType>(
        this IFixture fixture, 
        string paramName,
        TValueType value)
    {
        fixture.Customizations.Add(
           new ConstructorArgumentRelay<TTargetType, TValueType>(paramName, value)
        );
        return fixture;
    }
}

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

    public class TestClass<T>
    {
        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<T>
    {
        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<TestClass<string>, string>("value1", wanted)
               .ConstructorArgumentFor<TestClass<string>, string>("value2", wanted2);

        TestClass<string> t = fixture.Create<TestClass<string>>();
        SimilarClass<string> s = fixture.Create<SimilarClass<string>>();

        Assert.AreEqual(wanted,t.Value1);
        Assert.AreEqual(wanted2,t.Value2);
        Assert.AreNotEqual(wanted,s.Value1);
        Assert.AreNotEqual(wanted2,s.Value2);
    }        
}
于 2015-01-05T06:41:09.323 回答
8

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

首先要创建ISpecimenBuilder可以处理多个构造函数参数的东西

internal sealed class CustomConstructorBuilder<T> : ISpecimenBuilder
{
    private readonly Dictionary<string, object> _ctorParameters = new Dictionary<string, object>();

    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<object>();
        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<T>(this IFixture fixture, object parameters)
        {
            var builder = new CustomConstructorBuilder<T>();
            foreach (var prop in parameters.GetType().GetProperties())
            {
                builder.Addparameter(prop.Name, prop.GetValue(parameters));
            }

            fixture.Customize<T>(x => builder);
        }
    }

和用法:

var f = new Fixture();
f.FreezeActivator<UserInfo>(new { privateId = 15, parentId = (long?)33 });
于 2015-02-20T11:11:37.627 回答
1

好线程,我根据已经发布的许多问题添加了另一个转折:

用法

例子:

var sut = new Fixture()
    .For<AClass>()
    .Set("value1").To(aInterface)
    .Set("value2").ToEnumerableOf(22, 33)
    .Create();

测试类:

public class AClass
{
    public AInterface Value1 { get; private set; }
    public IEnumerable<int> Value2 { get; private set; }

    public AClass(AInterface value1, IEnumerable<int> value2)
    {
        Value1 = value1;
        Value2 = value2;
    }
}

public interface AInterface
{
}

全面测试

public class ATest
{
    [Theory, AutoNSubstituteData]
    public void ATestMethod(AInterface aInterface)
    {
        var sut = new Fixture()
            .For<AClass>()
            .Set("value1").To(aInterface)
            .Set("value2").ToEnumerableOf(22, 33)
            .Create();

        Assert.True(ReferenceEquals(aInterface, sut.Value1));
        Assert.Equal(2, sut.Value2.Count());
        Assert.Equal(22, sut.Value2.ElementAt(0));
        Assert.Equal(33, sut.Value2.ElementAt(1));
    }
}

基础设施

扩展方法:

public static class AutoFixtureExtensions
{
    public static SetCreateProvider<TTypeToConstruct> For<TTypeToConstruct>(this IFixture fixture)
    {
        return new SetCreateProvider<TTypeToConstruct>(fixture);
    }
}

参加流利风格的班级:

public class SetCreateProvider<TTypeToConstruct>
{
    private readonly IFixture _fixture;

    public SetCreateProvider(IFixture fixture)
    {
        _fixture = fixture;
    }

    public SetProvider<TTypeToConstruct> Set(string parameterName)
    {
        return new SetProvider<TTypeToConstruct>(this, parameterName);
    }

    public TTypeToConstruct Create()
    {
        var instance = _fixture.Create<TTypeToConstruct>();
        return instance;
    }

    internal void AddConstructorParameter<TTypeOfParam>(ConstructorParameterRelay<TTypeToConstruct, TTypeOfParam> constructorParameter)
    {
        _fixture.Customizations.Add(constructorParameter);
    }
}

public class SetProvider<TTypeToConstruct>
{
    private readonly string _parameterName;
    private readonly SetCreateProvider<TTypeToConstruct> _father;

    public SetProvider(SetCreateProvider<TTypeToConstruct> father, string parameterName)
    {
        _parameterName = parameterName;
        _father = father;
    }

    public SetCreateProvider<TTypeToConstruct> To<TTypeOfParam>(TTypeOfParam parameterValue)
    {
        var constructorParameter = new ConstructorParameterRelay<TTypeToConstruct, TTypeOfParam>(_parameterName, parameterValue);
        _father.AddConstructorParameter(constructorParameter);
        return _father;
    }

    public SetCreateProvider<TTypeToConstruct> ToEnumerableOf<TTypeOfParam>(params TTypeOfParam[] parametersValues)
    {
        IEnumerable<TTypeOfParam> actualParamValue = parametersValues;
        var constructorParameter = new ConstructorParameterRelay<TTypeToConstruct, IEnumerable<TTypeOfParam>>(_parameterName, actualParamValue);
        _father.AddConstructorParameter(constructorParameter);
        return _father;
    }
}

来自其他答案的构造函数参数中继:

public class ConstructorParameterRelay<TTypeToConstruct, TValueType> : ISpecimenBuilder
{
    private readonly string _paramName;
    private readonly TValueType _paramValue;

    public ConstructorParameterRelay(string paramName, TValueType paramValue)
    {
        _paramName = paramName;
        _paramValue = paramValue;
    }

    public object Create(object request, ISpecimenContext context)
    {
        if (context == null)
            throw new ArgumentNullException(nameof(context));
        ParameterInfo parameter = request as ParameterInfo;
        if (parameter == null)
            return new NoSpecimen();
        if (parameter.Member.DeclaringType != typeof(TTypeToConstruct) ||
            parameter.Member.MemberType != MemberTypes.Constructor ||
            parameter.ParameterType != typeof(TValueType) ||
            parameter.Name != _paramName)
            return new NoSpecimen();
        return _paramValue;
    }
}
于 2019-09-30T15:13:38.467 回答