AutoFixture.AutoMoq supply a known value for one constructor parameter

后端 未结 6 1302
悲哀的现实
悲哀的现实 2020-12-13 14:52

I\'ve just started to use AutoFixture.AutoMoq in my unit tests and I\'m finding it very helpful for creating objects where I don\'t care about the specific

相关标签:
6条回答
  • 2020-12-13 14:56

    You could do something like this. Imagine that you want to assign a particular value to the TimeSpan argument called 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;
        }
    }
    

    Imperatively, it could be used like this:

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

    This approach can be generalized to some degree, but in the end, we're limited by the lack of a strongly typed way to extract a ParameterInfo from a particular constructor or method argument.

    0 讨论(0)
  • 2020-12-13 15:02

    You have to replace:

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

    with:

    fixture.Inject("My known value");
    

    You can read more about Inject here.


    Actually the Freeze extension method does:

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

    Which means that the overload you used in the test actually called Create<T> with a seed: My known value resulting in "My known value4d41f94f-1fc9-4115-9f29-e50bc2b4ba5e".

    0 讨论(0)
  • 2020-12-13 15:09

    I fee like @Nick was almost there. When overriding the constructor argument, it needs to be for the given type and have it limited to that type only.

    First we create a new ISpecimenBuilder that looks at the "Member.DeclaringType" to keep the correct scope.

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

    Next we create an extension method to allow us to easily wire it up with 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;
        }
    }
    

    Now we create two similar classes to test with.

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

    Finally we test it with an extension of the original test to see that it will not override similarly named and typed constructor arguments.

    [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);
        }        
    }
    
    0 讨论(0)
  • 2020-12-13 15:10

    This seems to be the most comprehensive solution set here. So I'm going to add mine:

    The first thing to create ISpecimenBuilder that can handle multiple constructor parameters

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

    Then create extension method that simplifies usage of 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);
            }
        }
    

    And usage:

    var f = new Fixture();
    f.FreezeActivator<UserInfo>(new { privateId = 15, parentId = (long?)33 });
    
    0 讨论(0)
  • 2020-12-13 15:14

    So I'm sure people could work out the generalized implementation of Mark's suggestion but I thought I'd post it for comments.

    I've created a generic ParameterNameSpecimenBuilder based on 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;
        }
    }
    

    I've then defined a generic FreezeByName extension method on IFixture which sets the customization:

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

    The following test will now 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; }
    }
    
    0 讨论(0)
  • 2020-12-13 15:18

    Good thread, I added another twist based on many of the aswers already posted:

    Usage

    Example:

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

    Test classes:

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

    Full test

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

    Infrastructure

    Extension method:

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

    Classes participating in the fluent style:

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

    Constructor parameter relay from other answers:

    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;
        }
    }
    
    0 讨论(0)
提交回复
热议问题