在 AutoFixture 中将构造函数输入自定义为抽象 class

Customising constructor inputs to an abstract class in AutoFixture

在尝试对服务方法进行单元测试时,它实现的基本抽象 class 构造函数包含 2 个参数。我想在使用 Auto-fixture 调用服务时自定义这些参数。

基础服务代码如下

public abstract class ServiceBase : IHostedService, IDisposable
{
   private readonly CronExpression _expression;
   private readonly TimeZoneInfo _timeZoneInfo;

   protected BaseService(string cronExpression, TimeZoneInfo timeZoneInfo)
   {
      _expression = CronExpression.Parse(cronExpression);
      _timeZoneInfo = timeZoneInfo;
   }

   public abstract Task ExecuteTask(CancellationToken cancellationToken);
   .
   .
}

另一个服务继承自这个基础抽象class

public class TestService : ServiceBase
{
   public override async Task ExecuteTask(CancellationToken stoppingToken)
   {
      //Implementation here
   }
}

在我的单元测试中,我正在调用 ExecuteTask 函数,如下所示

Func<Task> executeAction = async () => await sut.ExecuteTask(A<CancellationToken>._);
executeAction.Should().NotThrow();

AutoFixture 尝试将随机字符串传递给 BaseService class 中的 CronExpression 构造函数参数。问题是这个 CronExpression 必须采用特定格式,否则在尝试解析它时会发生错误 CronExpression.Parse(cronExpression)

如何为构造函数参数传递自定义值?

提前致谢。

AutoFixture 不知道 class 的 string 参数的域约束。这应该暗示,也许这种类型不是 class 最合适的参数。这是 Primitive Obsession 的 classic 示例。

您可能会增强您的服务,而不是直接接受封装了这是 CronExpression 的想法的类型:

protected ServiceBase(CronExpression cronExpression, TimeZoneInfo timeZoneInfo)
{
    _cronExpression = cronExpression;
    ...
}

这样,可以保证 BaseService 将传递一个有效的 CronExpression。如果不是,那么 CronExpression 的创建将已经失败。

您可能认为我们只是转移了问题:您现在如何告诉 AutoFixture 创建有效的 CronExpression

将工作转移到构建 CronExpression 的优势在于,为创建有效 CronExpression 所做的任何定制现在都可以 重用 用于任何其他类型那将需要它。这将减少任何最终需要该类型的未来测试的仪式。更不用说避免原始痴迷的所有其他好处了。

关于告诉 AutoFixture 如何创建一个有效的CronExpression,有很多选项。您可以直接注入一个有效的:

fixture.Inject(CronExpression.Parse("myValidCronExpression"));

如果您希望能够 select 多个,您可以使用 ElementsBuilder 到 select 来自有效值的池:

public void Test()
{
    var fixture = new Fixture();
    fixture.Customizations.Add(new ElementsBuilder<CronExpression>(
        new[] { "validExpr1", "validExpr2" }
        .Select(s => CronExpression.Parse(s))
        .ToArray()))
    ...
}

通过创建 ISpecimenBuilder 的实现,可以最大程度地控制该类型的创建,我将在本答案中省略它。如果您想走那条路,这里和其他地方都有很多例子。

在对 AutoFixture 进行其中一项自定义后,您的 sut 的创建将起作用:

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

注意:我还没有测试 TimeZoneInfo 是否可以直接由 AutoFixture 的默认样本生成器实例化。如果不能,也可以对该类型使用类似的方法。