【问题标题】:AutoFixture.AutoMoq supply a known value for one constructor parameterAutoFixture.AutoMoq 为一个构造函数参数提供一个已知值
【发布时间】:2023-03-12 09:12:01
【问题描述】:

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

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

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

我想编写一个测试,为someValue 提供一个特定值,但让IServiceAutoFixture.AutoMoq 自动创建。

我知道如何在我的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&lt;T&gt;Write&lt;T&gt; 方法:

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

【问题讨论】:

  • 为什么要这样做?场景是什么? IME,像这样的场景在ExampleComponent 中往往闻起来像混合问题。 AutoFixture 不支持开箱即用是有原因的。
  • @MarkSeemann 您如何看待我在编辑问题中的场景?我不认为这可以解释为混合问题。
  • 嗯,这对我来说很难说,因为我不明白你打算如何使用lifespanlifespan 不需要和当前时间交互吗?一旦你开始思考诸如此类的问题,也许仍然会出现抽象。上次我做这样的事情时,我到达了一个 ILease 接口,这使得缓存逻辑更加灵活,因为我现在可以支持:Absolute Expiry、Sliding Window Expiry、LRU Expiry 和许多其他选项。
  • @MarkSeemann 我喜欢 ILease 的声音,当我说我的解决方案不能被解释为混合问题时,我认为我必须纠正。自从编辑问题以来,我在CookieCache 中添加了一个IDateTimeProvider 依赖项,并通过将lifespan 添加到当前日期来设置cookie 的到期日期。我现在意识到这确实是一个混合问题,尽管这种混合问题只需要一行代码!

标签: c# unit-testing autofixture automocking


【解决方案1】:

你必须替换:

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

与:

fixture.Inject("My known value");

你可以阅读更多关于Injecthere的信息。


实际上Freeze 扩展方法可以做到:

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

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

【讨论】:

  • FWIW,虽然这个答案很好地解释了 FreezeInject 的工作原理,但建议的解决方案将导致 all 字符串获取值“我的已知值",这可能不是 OP 想要的。
  • 感谢@NikosBaxevanis,完美地回答了我的问题。
  • @MarkSeemann - 有什么方法可以使用 AutoFixture 在构造函数中设置相同类型的不同已知值?
  • @MarkSeemann 当一个类具有两个不同的字符串依赖项(或任何类型的两个依赖项)并且需要将它们设置为不同的值以进行单元测试时,肯定存在合法的情况吗?例如,我需要生成一个具有两种不同容量的购物篮 style 解决方案(不是 实际的购物篮):一种类型的最多 X 个项目和最多 Y 个项目另一种类型。我需要在单元测试中将它们设置为不同的值,因此我使用了构建器类,而不是使用 AutoFixture 进行这些测试。
【解决方案2】:

可以做这样的事情。想象一下,您想为名为@9​​87654322@ 的TimeSpan 参数分配一个特定值。

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 的强类型方法。

【讨论】:

  • 感谢您。我想我会在此基础上添加一个通用的扩展方法,以便在以后的测试中使用。
【解决方案3】:

我觉得@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);
    }        
}

【讨论】:

    【解决方案4】:

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

    用法

    例子:

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

    【讨论】:

      【解决方案5】:

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

      首先创建可以处理多个构造函数参数的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 });
      

      【讨论】:

      • 漂亮干净的解决方案。在我们的项目中,有时我们对同一类型有多个依赖项,而我们只想测试其中一个,这个命名参数非常适合我们的需求。感谢分享。
      【解决方案6】:

      所以我相信人们可以制定出马克建议的通用实施,但我想我会把它发布给 cmets。

      我基于 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;
          }
      }
      

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

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

      【讨论】:

      • 工作就像一个魅力。做了一点混合重载,冻结值仍然自动生成:public static T FreezeByName&lt;T&gt;(this IFixture fixture, string name) { var value = fixture.Create&lt;T&gt;(); fixture.Customizations.Add(new ParameterNameSpecimenBuilder&lt;T&gt;(name, value)); return value; }
      • 这太棒了!谢谢。
      • 这很酷,谢谢尼克。我认为我可以解决的唯一问题是,当您使用相同的夹具实例创建另一个类实例时,该实例在字符串类型的构造函数中也有一个“名称”参数。发生这种情况的可能性很小,但只是说。你同意吗?
      • 是的,我同意这是一个限制,但我同意它也不太可能给您带来任何实际问题。
      猜你喜欢
      • 1970-01-01
      • 2018-05-13
      • 2015-05-25
      • 2021-12-17
      • 1970-01-01
      • 2015-12-27
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多