Search code examples
c#unit-testingautofixturensubstitute

Override Autofixture customization setup


I've a class with several services injected in its constructor. I'm using Autofixture with xUnit.net and NSubstitute, and created an attribute to setup the global customization.

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

    }
}

I also have a custom customization class that setups the common customization for the test methods in the same 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;
        });
    }
}

Now, some of my tests depend on modifying specific properties in the objects returned by the services. So in some cases, I want to modify the RevenueContext and in some cases, I want to modify the Company data.

What I did was creating another object inside the test itself and modify the Returns of the service with the new object, like this:

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

But this doesn't work. The RevenueContext from the RevenueProviderCustomization is still used.

Does anyone know how I can override the return from the service? I don't want to setup the fixture one by one in my test, so I was hoping to be able to create a 'general setup' and modify as needed according to the test case.

UPDATE 1

Trying the answer from Mark, I changed the test to

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

The problem is because the RevenueContext is called in the RevenueProvider constructor. So my modification to the DepartmentId happens after the call was made.

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

Solution

  • Assuming that RevenueProvider essentially looks like this:

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

    Then the following test passes:

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

    This scenario is exactly what the [Frozen] attribute is designed to handle. The various attributes that AutoFixture defines are applied in the order of the arguments. This is by design, because it enables you to pull out a few values from the argument list before you freeze a type.

    In the OP, [Frozen] is only applied after sut, which is the reason the configuration of the mock doesn't apply within the SUT.