簡體   English   中英

AutoFixture.AutoMoq為一個構造函數參數提供已知值

[英]AutoFixture.AutoMoq supply a known value for one constructor parameter

我剛開始在我的單元測試中使用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<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...  
}

我正在實現一個CookieCache ,其中ITypeConverter處理對象與字符串之間的轉換, 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的有效期編寫測試時,我關心的是生命周期而不是轉換器。

所以我相信人們可以解決馬克建議的普遍實施,但我想我會發表評論。

我已經基於Mark的LifeSpanArg創建了一個通用的ParameterNameSpecimenBuilder

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; }
}

你必須替換:

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>我的已知值導致“我的已知 4d41f94f-1fc9-4115-9f29-e50bc2b4ba5e”

可以這樣做。 想象一下,您希望為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的限制。

我的費用就像@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);
    }        
}

這似乎是這里設置最全面的解決方案。 所以我要添加我的:

首先要創建可以處理多個構造函數參數的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 });

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM