覆盖 Autofixture 自定义设置

Override Autofixture customization setup

我有一个 class,在其构造函数中注入了多个服务。我将 Autofixture 与 xUnit.net 和 NSubstitute 一起使用,并创建了一个属性来设置全局自定义。

public class AutoDbDataAttribute : AutoDataAttribute
{
    public AutoDbDataAttribute() : base(() => new Fixture().Customize(new AutoNSubstituteCustomization()))
    {

    }

    public AutoDbDataAttribute(Type customizationType) : base(() =>
    {
        var customization = Activator.CreateInstance(customizationType) as ICustomization;

        var fixture = new Fixture();
        fixture.Customize(new AutoNSubstituteCustomization());
        fixture.Customize(customization);

        return fixture;
    })
    {

    }
}

我还有一个自定义 class,它在相同的 class 中为测试方法设置通用自定义。

public class RevenueProviderCustomization : ICustomization
{
    public void Customize(IFixture fixture)
    {
        fixture.Register<IRevenueContextService>(() =>
        {
            var contextService = Substitute.For<IRevenueContextService>();
            contextService.GetContext().Returns(fixture.Create<RevenueContext>());
            return contextService;
        });

        fixture.Register<ICompanyService>(() =>
        {
            var companyService = Substitute.For<ICompanyService>();
            companyService.Get(Arg.Any<Guid>()).Returns(fixture.Create<Company>());
            return companyService;
        });
    }
}

现在,我的一些测试依赖于修改服务 return 对象中的特定属性。所以在某些情况下,我想修改RevenueContext,在某些情况下,我想修改公司数据。

我所做的是在测试本身内部创建另一个对象并使用新对象修改服务的 Returns,如下所示:

[Theory]
[AutoDbData(typeof(RevenueProviderCustomization))]
public void ShouldReturnCompanyRevenue(RevenueProvider sut, Company company, [Frozen]IRevenueContextService contextService)
{
    var fixture = new Fixture();
    RevenueContext context = fixture.Build<RevenueContext>().With(c => c.DepartmentId, null).Create();
    contextService.GetContext().Returns(context);

    sut.GetRevenue().Should().Be(company.Revenue);
}

但这不起作用。 RevenueProviderCustomization 中的 RevenueContext 仍在使用。

有谁知道如何覆盖服务中的 return?我不想在我的测试中一个一个地设置夹具,所以我希望能够根据测试用例创建一个 'general setup' 并根据需要进行修改。

更新 1

尝试马克的回答,我将测试更改为

[Theory]
    [AutoDbData(typeof(RevenueProviderCustomization))]
    public void ShouldReturnCompanyRevenue([Frozen]IRevenueContextService contextService, [Frozen]Company company, RevenueProvider sut, RevenueContext context)
    {
        context.DepartmentId = null;
        contextService.GetContext().Returns(context);

        sut.GetRevenue().Should().Be(company.Revenue);
    }

问题是因为在 RevenueProvider 构造函数中调用了 RevenueContext。所以我对 DepartmentId 的修改发生在调用之后。

public RevenueProvider(IRevenueContextService contextService, ICompanyService companyService)
    {
        _contextService = contextService;
        _companyService = companyService;

        _company = GetCompany();
    }

    public double GetRevenue()
    {
        if (_hasDepartmentContext)
            return _company.Departments.Single(d => d.Id == _departmentId).Revenue;
        else
            return _company.Revenue;
    }

    private Company GetCompany()
    {
        RevenueContext context = _contextService.GetContext();

        if (context.DepartmentId.HasValue)
        {
            _hasDepartmentContext = true;
            _departmentId = context.DepartmentId.Value;
        }

        return _companyService.Get(context.CompanyId);
    }

假设 RevenueProvider 基本上是这样的:

public class RevenueProvider
{
    private readonly ICompanyService companySvc;

    public RevenueProvider(ICompanyService companySvc)
    {
        this.companySvc = companySvc;
    }

    public object GetRevenue()
    {
        var company = this.companySvc.Get(Guid.Empty);
        return company.Revenue;
    }
}

然后下面的测试通过:

[Theory]
[AutoDbData(typeof(RevenueProviderCustomization))]
public void ShouldReturnCompanyRevenue(
    [Frozen]ICompanyService companySvc,
    RevenueProvider sut,
    Company company)
{
    companySvc.Get(Arg.Any<Guid>()).Returns(company);
    var actual = sut.GetRevenue();
    Assert.Equal(company.Revenue, actual);
}

这种情况正是 [Frozen] 属性旨在处理的问题。 AutoFixture 定义的各种属性按参数的顺序应用。这是设计使然,因为它使您能够在冻结类型之前从参数列表中提取一些值。

在 OP 中,[Frozen] 仅在 sut 之后应用,这就是模拟配置不适用于 SUT 的原因。