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