是否可以组合 Autofixture 定制(通过 NUnit3 属性)?如果可以,如何组合?

Can Autofixture customizations (via NUnit3 attributes) be composed and if so - how?

我正在进行一些测试,我想在其中编写一些封装在 Autofixture 自定义中的对象设置逻辑 类。这是我所拥有的相关部分:

public class UseSpecificConstructorCustomization : ICustomization
{
    public void Customize(IFixture fixture) 
    {
        fixture.Customize<MyObject>(c => c.FromFactory((string s) => new MyObject(s)));
    }
}

public class ExecuteMethodCustomization : ICustomization
{
    public void Customize(IFixture fixture) 
    {
        fixture.Customize<MyObject>(c => c.Do(x => x.AMethodIWantToExecute()));
    }
}

对于这些定制中的每一个,我都有相应的 CusomizeAttribute 实现,其中 returns 定制实例。现在在我的测试中(NUnit3.x)我想做这样的事情:

[Test,AutoData]
public void ThisIsMyTestMethod([UseSpecificConstructor,ExecuteMethod] MyObject obj)
{
    // Here I would like the object to have been created using the specified
    // string constructor AND the method 'AMethodIWantToExecute' to have been executed.
}

不过,我看到的行为表明,在这种情况下,只使用了两种自定义设置中的一种 - 似乎是最后指定的那个。这在某种程度上是有道理的,因为 Autofixture 是关于 "creating" 对象的,而不是在创建后修改它们。将两个自定义项(在内部对应于样本生成器)用于创建同一对象没有意义。

有没有办法使用 Autofixture 来实现我所描述的?我想我正在寻找的是一种类似于定制的机制,它不使用样本生成器,而是公开一个 "specimen customizer",以 post-在样本生成器完成其后处理对象工作。这种机制只能设置属性并使用 .Do()(不能使用 .FromFactory() 等)。如果它不存在,那么我可能会去请求它的功能,但也许它确实存在,但我没有看到它。

我意识到我也可以将这个 "object configuration after creation" 逻辑移到测试本身中,但它不止几行那么长,我不想重复它。此外 - 设置逻辑与测试并不特别相关,它只是让对象进入基本有效状态。 "Cognitively speaking" 该逻辑属于对象创建逻辑; IE:开发人员不想考虑它。

围绕我自己的源代码进行了更多research/poking,我想我找到了答案。解决方案在于 Fixture behaviors.

行为是 decorator objects around ISpecimenBuilder and so they can post-process the result from a specimen builder and perform extra work. Behaviors implement the interface ISpecimenBuilderTransformation. It seems that at most one specimen builder can fulfil a request for Autofixture to create an object (following chain of responsibility 模式),但 可以在此之上应用无限数量的 行为,执行各种任务,例如修改之前的结果它被退回了。

行为是 added/removed to/from 通过 fixture.Behaviors 的夹具。

2021 年 4 月更新:此技术的一个示例

我现在已经在一个开源项目中使用了这种技术 can link to that as an example。请注意,链接的项目正在使用 XUnit,但您会看到属性和语法与 NUnit3 Autofixture 集成相同。

下面列出了相关部分。

建议:将您的属性命名为“WithXyz”

CustomizeAttribute 的实施使用标本生成器转换(也称为“行为”)实际上并没有创建标本,而是对其进行了修改。因此,根据属性的修改方式而不是创建方式来命名属性。

创建一个post-加工样本命令class

Autofixture 提供了一个名为ISpecimenCommand 的接口,用于对样本执行post-处理。创建一个实现此接口并包装委托的小 class。我发现使该命令通用化很有用且方便:

public class PostprocessingCommand<T> : ISpecimenCommand where T : class
{
  readonly Action<T,ISpecimenContext> builderCustomizer;
  
  public void Execute(object specimen, ISpecimenContext context)
  {
    var builder = (T) specimen;
    builderCustomizer(builder, context);
  }
  
  public PostprocessingCommand(Action<T,ISpecimenContext> builderCustomizer)
  {
    this.builderCustomizer = builderCustomizer ?? throw new ArgumentNullException(nameof(builderCustomizer));
  }
}

创建仅匹配正确类型的规范

Autofixture 再次有一个名为 IRequestSpecification 的接口,用于匹配样本请求。目的是确定是否应在所得样本上使用给定的 post 处理操作。

了解规范 匹配标本请求而不是创建的标本本身 非常重要。样本请求通常是 System.Type.

的实例
public class IsInstanceOf : IRequestSpecification
{
  readonly Type type;

  public bool IsSatisfiedBy(object request) => request != null && type.IsAssignableFrom(request);

  public IsInstanceOf(Type type)
  {
    this.type = type ?? throw new ArgumentNullException(nameof(type));
  }
}

您的标本生成器转换将这些结合在一起

样本生成器转换(又名 行为)class 将样本命令和请求规范结合在一起,并使用 Autofixture 的内置 Postprocessor class。我再次发现将其设为通用很方便。

public class PostprocessingTransformation<T> : ISpecimenBuilderTransformation where T : class
{
  readonly Action<T,ISpecimenContext> builderCustomizer;

  public ISpecimenBuilderNode Transform(ISpecimenBuilder builder)
  {
    var command = new PostprocessingCommand<T>(builderCustomizer);
    var spec = new IsInstanceOf(typeof(T));
                
    return new Postprocessor(builder, command, spec);
  }

  public PostprocessingTransformation(Action<T,ISpecimenContext> builderCustomizer)
  {
    this.builderCustomizer = builderCustomizer ?? throw new ArgumentNullException(nameof(builderCustomizer));
  }
}

编写自定义以使用委托创建行为实例

一个小的自定义 class 将用于实例化行为实例并将其传递给 Action<T,ISpecimenContext>,后者将对适当的样本执行实际的 post 处理。

它是此自定义的一个实例,应从您的 CustomizeAttribute 实施中返回。

public class MySpecificCustomization : ICustomization
{
  public void Customize(IFixture fixture)
  {
    var behavior = new PostprocessingTransformation<MySpecificType>((specimen, context) => {
      // Write your logic for modifying an instance of MySpecificType
      // here.  It will be the parameter named 'specimen'.
      // You may also use 'context' to access other Autofixture features.
    });
    fixture.Behaviors.Add(behavior);
  }
}