AutoFixture.AutoMoq fournit une valeur connue pour un paramètre de constructeur

je viens de commencer à utiliser AutoFixture.AutoMoq dans mes tests unitaires et je le trouve très utile pour créer des objets où je ne me soucie pas de la valeur spécifique. Après tout, la création anonyme d'objets est ce dont il s'agit.

ce qui me pose problème, c'est quand je me soucie d'un ou plusieurs paramètres du constructeur. Prendre ExampleComponent ci-dessous:

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

je veux écrire un essai où je fournir une valeur spécifique pour someValue mais laissez IService pour être créé automatiquement par AutoFixture.AutoMoq.

je sais comment utiliser Freeze sur mon IFixture conserver une valeur connue, qui sera injecté dans un composant, mais je n'arrive pas à voir comment alimentation une valeur connue de mon propre.

voici ce que j'aimerais idéalement faire:

[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
    ...
}

je sais que je pourrais le faire en appelant directement le constructeur mais ce ne serait plus de la création d'objet anonyme. Est-il un moyen pour utiliser AutoFixture.AutoMock comme ceci ou Ai-je besoin d'incorporer un conteneur DI dans mes tests pour être en mesure de faire ce que je veux?


EDIT:

j'aurais probablement dû faire moins d'absract dans ma question initiale donc voici mon scénario spécifique.

j'ai un ICache interface génériques TryRead<T> et Write<T> méthodes:

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

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

    // other methods not shown...  
}

je suis à la mise en œuvre d'un CookieCacheITypeConverter gère la conversion des objets vers et à partir de chaînes et lifespan est utilisé pour définir la date d'expiration d'un 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...
}

donc en écrivant un test pour la date d'expiration d'un cookie, je me soucie de la durée de vie mais pas tellement du convertisseur.

20
demandé sur Nick Soper 2013-05-29 20:37:04

5 réponses

vous devez remplacer:

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

avec:

fixture.Inject("My known value");

vous pouvez en savoir plus sur Injectici.


en Fait, le Freeze la méthode d'extension fait:

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

ce qui signifie que la surcharge que vous avez utilisée dans le test a en fait appelé Create<T> avec une graine: Ma valeur connue entraînant "Mon connu value4d41f94f-1fc9-4115-9f29-e50bc2b4ba5e".

13
répondu Nikos Baxevanis 2013-05-29 17:31:48

donc je suis sûr que les gens pourraient travailler sur la mise en œuvre généralisée de la suggestion de Mark, mais j'ai pensé que je voudrais le poster pour des commentaires.

j'ai créé un générique ParameterNameSpecimenBuilder basé sur Mark's 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;
    }
}

j'ai alors défini unFreezeByName méthode d'extension sur IFixture qui définit la personnalisation:

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

Le test suivant, maintenant pass:

[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; }
}
20
répondu Nick Soper 2013-06-06 08:28:49

Vous faire quelque chose comme cela. Imaginez que vous voulez attribuer une valeur particulière à TimeSpan argument appelé 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;
    }
}

Impérativement, il pourrait être utilisé comme ceci:

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

var sut = fixture.Create<CookieCache>();

cette approche peut être généralisée dans une certaine mesure, mais en fin de compte, nous sommes limités par l'absence d'une manière fortement typée d'extraire un ParameterInfo d'un argument de constructeur ou de méthode particulier.

10
répondu Mark Seemann 2013-06-06 06:01:16

j'ai l'impression que @ Nick était presque là. Quand on supplante l'argument du constructeur, il faut qu'il soit pour le type donné et qu'il soit limité à ce type seulement.

nous créons D'abord un nouveau ISpecimenBuilder qui regarde le "membre".DeclaringType" pour garder la bonne portée.

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

ensuite, nous créons une méthode d'extension pour nous permettre de la raccorder facilement avec 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;
    }
}

maintenant nous créons deux classes similaires pour tester avec.

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

finalement, nous le testons avec une extension du test original pour voir qu'il ne supplantera pas les arguments du constructeur nommés et dactylographiés de la même manière.

[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);
    }        
}
8
répondu ExCodeCowboy 2015-01-05 06:47:36

Cela semble être la solution la plus complète définie ici. Je vais donc ajouter le mien:

La première chose à créer ISpecimenBuilder qui peut gérer plusieurs paramètres du constructeur

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

puis créer une méthode d'extension qui simplifie l'utilisation de created builder

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

Et d'utilisation:

var f = new Fixture();
f.FreezeActivator<UserInfo>(new { privateId = 15, parentId = (long?)33 });
5
répondu Alexander Kozlov 2015-02-20 11:11:37