The short takeaway now that the solution has been found:
AutoFixture returns frozen the mock just fine; my sut that was also generated by AutoFixture ju
In the first test you can create an instance of the Fixture
class with the AutoMoqCustomization
applied:
var fixture = new Fixture()
.Customize(new AutoMoqCustomization());
Then, the only changes are:
Step 1
// The following line:
Mock<ISettings> settingsMock = new Mock<ISettings>();
// Becomes:
Mock<ISettings> settingsMock = fixture.Freeze<Mock<ISettings>>();
Step 2
// The following line:
ITracingService tracing = new Mock<ITracingService>().Object;
// Becomes:
ITracingService tracing = fixture.Freeze<Mock<ITracingService>>().Object;
Step 3
// The following line:
IMappingXml sut = new SettingMappingXml(settings, tracing);
// Becomes:
IMappingXml sut = fixture.CreateAnonymous<SettingMappingXml>();
That's it!
Here is how it works:
Internally, Freeze
creates an instance of the requested type (e.g. Mock<ITracingService>
) and then injects it so it will always return that instance when you request it again.
This is what we do in
Step 1
andStep 2
.
In Step 3
we request an instance of the SettingMappingXml
type which depends on ISettings
and ITracingService
. Since we use Auto Mocking, the Fixture
class will supply mocks for these interfaces. However, we have previously injected them with Freeze
so the already created mocks are now automatically supplied.
Assuming that the SettingKey
property is defined as follows, I can now reproduce the issue:
public string SettingKey { get; set; }
What happens is that the Test Doubles injected into the SettingMappingXml instance are perfectly fine, but because the SettingKey
is writable, AutoFixture's Auto-properties feature kicks in and modifies the value.
Consider this code:
var fixture = new Fixture().Customize(new AutoMoqCustomization());
var sut = fixture.CreateAnonymous<SettingMappingXml>();
Console.WriteLine(sut.SettingKey);
This prints something like this:
SettingKey83b75965-2886-4308-bcc4-eb0f8e63de09
Even though all the Test Doubles are properly injected, the expectation in the Setup
method isn't met.
There are many ways to address this issue.
Protect invariants
The proper way to resolve this issue is to use the unit test and AutoFixture as a feedback mechanism. This is one of the key points in GOOS: problems with unit tests are often a symptom about a design flaw rather than the fault of the unit test (or AutoFixture) itself.
In this case it indicates to me that the design isn't fool-proof enough. Is it really appropriate that a client can manipulate the SettingKey
at will?
As a bare minimum, I would recommend an alternative implementation like this:
public string SettingKey { get; private set; }
With that change, my repro passes.
Omit SettingKey
If you can't (or won't) change your design, you can instruct AutoFixture to skip setting the SettingKey
property:
IMappingXml sut = fixture
.Build<SettingMappingXml>()
.Without(s => s.SettingKey)
.CreateAnonymous();
Personally, I find it counterproductive to have to write a Build
expression every time I need an instance of a particular class. You can decouple how the SettingMappingXml
instance are created from the actual instantiation:
fixture.Customize<SettingMappingXml>(
c => c.Without(s => s.SettingKey));
IMappingXml sut = fixture.CreateAnonymous<SettingMappingXml>();
To take this further, you can encapsulate that Customize
method call in a Customization.
public class SettingMappingXmlCustomization : ICustomization
{
public void Customize(IFixture fixture)
{
fixture.Customize<SettingMappingXml>(
c => c.Without(s => s.SettingKey));
}
}
This requires you to create your Fixture
instance with that Customization:
IFixture fixture = new Fixture()
.Customize(new SettingMappingXmlCustomization())
.Customize(new AutoMoqCustomization());
Once you get more than two or three Customizations to chain, you may get tired of writing that method chain all the time. It's time to encapsulate those Customizations into a set of conventions for your particular library:
public class TestConventions : CompositeCustomization
{
public TestConventions()
: base(
new SettingMappingXmlCustomization(),
new AutoMoqCustomization())
{
}
}
This enables you to always create the Fixture
instance like this:
IFixture fixture = new Fixture().Customize(new TestConventions());
The TestConventions
gives you a central place where you can go and occasionally modify your conventions for the test suite when you need to do so. It reduces the maintainability tax of your unit tests and helps keep the design of your production code more consistent.
Finally, since it looks as though you are using xUnit.net, you could utilize AutoFixture's xUnit.net integration, but before you do that you'd need to use a less imperative style of manipulating the Fixture
. It turns out that the code which creates, configures and injects the ISettings
Test Double is so idiomatic that it has a shortcut called Freeze:
fixture.Freeze<Mock<ISettings>>()
.Setup(s => s.Get(settingKey)).Returns(xmlString);
With that in place, the next step is to define a custom AutoDataAttribute:
public class AutoConventionDataAttribute : AutoDataAttribute
{
public AutoConventionDataAttribute()
: base(new Fixture().Customize(new TestConventions()))
{
}
}
You can now reduce the test to the bare essentials, getting rid of all the noise, enabling the test to succinctly express only what matters:
[Theory, AutoConventionData]
public void ReducedTheory(
[Frozen]Mock<ISettings> settingsStub,
SettingMappingXml sut)
{
string xmlString = @"
<mappings>
<mapping source='gcnm_loan_amount_min' target='gcnm_loan_amount_min_usd' />
<mapping source='gcnm_loan_amount_max' target='gcnm_loan_amount_max_usd' />
</mappings>";
string settingKey = "gcCreditApplicationUsdFieldMappings";
settingsStub.Setup(s => s.Get(settingKey)).Returns(xmlString);
XElement actualXml = sut.GetXml();
XElement expectedXml = XElement.Parse(xmlString);
Assert.True(XNode.DeepEquals(expectedXml, actualXml));
}
Other options
To make the original test pass, you could also just switch off Auto-properties entirely:
fixture.OmitAutoProperties = true;