Unit Testing: Activate Easy Mode
The Four Horsemen: AutoMock, AutoFixture, Shouldly, and xUnit
Introduction
Yesterday, after presenting to the Willamette Valley Software Engineering meetup about Dependency Injection, an engineer by the name of Josh and I got to talking. After making an offhand comment about a testing framework combination I'd used, he got really excited. I figured I'd use this as an excuse to do a write-up on what I've been doing.
After being frustrated with setting up the same mock over and over again, or newing up a mock when I need something slightly different, I did some digging and found three tools to make life more bearable when testing with xUnit: AutoMock, AutoFixture, and Shouldly.
Since introducing this method to my team, they have commented on how much testing boilerplate is removed, and how less fragile the test suites are.
"Normal" method of testing
Let's say we have a service that has logging, data, and time dependencies.
public class PersonService
{
private readonly ITime _time;
private readonly ILogger<PersonService> _logger;
private readonly IPersonRepository _people;
public PersonService(
ITime time,
ILogger<PersonService> logger,
IPersonRepository people)
{
_time = time;
_logger = logger;
_people = people;
}
public bool IsAdult(string personId)
{
var person = _people.Get(personId);
return person.DateOfBirth.AddYears(18) < _time.NowUtc();
}
/* Other methods omitted */
}
Our test might look like this:
using Moq;
using Xunit;
public class PersonServiceTests
{
[Fact]
public void GivenPersonUnder18_IsAdultShouldReturnFalse()
{
// arrange
var id = "12345";
var now = new DateTime(2019, 01, 01);
var subject = new Person {DateOfBirth = new DateTime(2005, 01, 01)};
var time = new Mock<ITime>();
var logger = new Mock<ILogger<PersonService>>();
var people = new Mock<IPersonRepository>();
time.Setup(x => x.NowUtc()).Returns(now);
people.Setup(x => x.Get(It.IsAny<string>())).Returns(subject);
var sut = new PersonService(time.Object, logger.Object, people.Object);
// act
var result = sut.IsAdult(id);
// assert
Assert.False(result);
}
}
Reducing setup
There are a couple of helpful libraries that I'll combine with a little extra custom code to reduce setup.
AutoMoq
AutoMoq leverages a container to inject Mocked dependencies into our test code allowing us to focus on only the logic we care about. We also create less brittle tests, as the SUT's constructor arguments become less important.
AutoFixture
AutoFixture will "create values of virtually any type type without the need for you to explicitly define which values should be used."It will generate random fake data as needed, allowing us to focus on the important logic.
I highly recommend diving into both frameworks to understand the internals of what they are doing, but we can create a testing attribute that combines the two to Automatically Mock any dependencies and automatically create objects for us.
We do this by inheriting from AutoFixture's AutoDataAttribute
and creating a new AutoMoq fixture when the attribute is used:
public class AutoMockAttribute : AutoDataAttribute
{
public AutoMockAttribute()
: base(() => new Fixture().Customize(new AutoMoqCustomization())) { }
}
When using this attribute, by default, any dependency will be generated by AutoFixture when encountered. We can control a specific Mock by using the [Frozen] attribute on the method parameter, as you'll see below. This means we only need declare Mocks that are important to testing.
Now we can rewrite our test to look a little cleaner, injecting our moqs and fixtures in the method:
using AutoFixture;
using AutoFixture.AutoMoq;
using AutoFixture.Xunit2;
using Moq;
using Xunit;
[Theory, AutoMock]
public void GivenPersonUnder18_IsAdultShouldReturnFalse(
[Frozen] Mock<ITime> time,
[Frozen] Mock<IPersonRepository> people,
PersonService sut)
{
// arrange
var id = "12345";
var now = new DateTime(2019, 01, 01);
var subject = new Person {DateOfBirth = new DateTime(2005, 01, 01)};
time.Setup(x => x.NowUtc()).Returns(now);
people.Setup(x => x.Get(It.IsAny<string>())).Returns(subject);
// act
var result = sut.IsAdult(id);
// assert
Assert.False(result);
}
}
Notice:
- We changed this from a [Fact]
to a [Theory]
- We are no longer mocking the logger as our test isn't concerned with logging
We can leverage the AutoFixture to further reduce boilerplate by eliminating other pieces we don't care about:
using AutoFixture;
using AutoFixture.AutoMoq;
using AutoFixture.Xunit2;
using Moq;
using Xunit;
[Theory, AutoMock]
public void GivenPersonUnder18_IsAdultShouldReturnFalse(
string id,
Person subject,
[Frozen] Mock<ITime> time,
[Frozen] Mock<IPersonRepository> people,
PersonService sut)
{
// arrange
var now = new DateTime(2019, 01, 01);
subject.DateOfBirth = new DateTime(2005,01,01);
time.Setup(x => x.NowUtc()).Returns(now);
people.Setup(x => x.Get(It.IsAny<string>())).Returns(subject);
// act
var result = sut.IsAdult(id);
// assert
Assert.False(result);
}
We've now moved the Person instantiation to AutoFixture and controlled only the properties we want to test.
Fluent Style Assertions
Now we've focused in on the meat of the test and what we are concerned with.The final step is adding a more fluent style to the assertions, which increases readability. There are a few fluent assertion libraries out there, but after using a few, I've settled on Shouldly.
This isn't a necessary step, but I feel it takes a cognitive load off of trying to parse what a test is trying to get at. Compare:
// using Xunit assertions
var result = sut.IsAdult(id);
Assert.False(result);
// using Shouldly
sut.IsAdult(id).ShouldBeFalse();
using AutoFixture;
using AutoFixture.AutoMoq;
using AutoFixture.Xunit2;
using Moq;
using Xunit;
using Shouldly;
[Theory, AutoMock]
public void GivenPersonUnder18_IsAdultShouldReturnFalse(
string id,
Person subject,
[Frozen] Mock<ITime> time,
[Frozen] Mock<IPersonRepository> people,
PersonService sut)
{
// arrange
var now = new DateTime(2019, 01, 01);
subject.DateOfBirth = new DateTime(2005,01,01);
time.Setup(x => x.NowUtc()).Returns(now);
people.Setup(x => x.Get(It.IsAny<string>())).Returns(subject);
// act & assert
sut.IsAdult(id).ShouldBeFalse();
}
Conclusion
This combination isn't a silver bullet. In fact, I've hit some odd roadblocks when trying to take this approach with MVC Controllers. For a lot of cases, though, it reduces a lot of the testing boilerplate and allows the tester to focus on the really important part: testing the logic. Additionally, my team has found it invaluable as dependencies change due to new requirements and refactoring as the refactor won't break the test.
I hope this has been useful, and thank you, Josh, for the interest.
Software Development Nerd