4

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);
    }
adelb
  • 791
  • 7
  • 26
  • What do you mean that 'it doesn't work'? It'd be helpful if you showed `RevenueProvider`, so that we understand what you're trying to test. – Mark Seemann Mar 31 '18 at 07:50
  • Basically the RevenueProvider should return a revenue of a Company OR a Department in the company. Depends on the RevenueContext that is returned by IRevenueContextService. If the RevenueContext has a DepartmentId, then the Department's revenue should be returned. If the RevenueContext DepartmentId is null, then the revenue of the Company should be returned. That's why I need to modify the RevenueContext object returned by the mock IRevenueContextService in some cases. Because I need the DepartmentId to be null – adelb Mar 31 '18 at 09:13

1 Answers1

2

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.

Mark Seemann
  • 225,310
  • 48
  • 427
  • 736
  • Unfortunately still doesn't work, I changed the IRevenueContextService to be before sut but the returned RevenueContext still contains DepartmentId, see UPDATE 1 in the post – adelb Apr 03 '18 at 07:04
  • The problem is because the call to RevenueContext is done as part of the constructor. So the modification to the DepartmentId happens afterwards.. – adelb Apr 03 '18 at 15:01
  • @AdeliaBenalius I'm not sure you can do much about that, then, but [don't perform work in constructors](http://blog.ploeh.dk/2011/03/03/InjectionConstructorsshouldbesimple). – Mark Seemann Apr 03 '18 at 20:49
  • @AdeliaBenalius Or rather, I can't think of a way to address that issue with `[AutoData]`, but still enabling after-the-fact configuration in the test itself. I think you'll either have to change the constructor so that it doesn't do eager work, or you'll have to write the test in a more imperative fashion. – Mark Seemann Apr 03 '18 at 21:02
  • yes I figured I'd have to do that. Thanks for the help Mark! – adelb Apr 04 '18 at 09:15
  • @AdeliaBenalius Sorry that I couldn't offer better help than that! – Mark Seemann Apr 04 '18 at 14:25
  • You already did Mark! What you said about the Frozen object needs to be before sut object helped! – adelb Apr 04 '18 at 17:54