When it comes to unit testing .NET applications, there are various frameworks, test runners and libraries available. Teams often undergo meticulous efforts to write the right testing strategy for their applications. We in the Odyssey Team, here at Trainline, have been experimenting with a combination of XUnit, Moq and AutoFixture. These technologies fulfil our needs for writing test suites that are much easier to understand, are robust and are quicker to write.
This short walkthrough will give you a brief introduction to each technology and show you how we’ve been working with them.
XUnit is a .NET test runner created by the developers that built NUnit. The features that XUnit has to offer came from the lessons learnt from using NUnit 2.0. They aimed at improving test isolation and trying to codify a set of rules to establish a testing standard.
Start by creating a new class project and installing XUnit by running install-package xunit in the package manager console. For this example, I will be using the console runner, which I’ll install using install-package xunit.runner.console. Various other runners are readily available.
The setup code in XUnit is much more succinct than that found in NUnit test suites. Tests can be broken down into Fact or Theory. Facts are always true, testing the invariant conditions. Theories are only true for certain sets of data.
Another major change from NUnit is the removal of [Setup] and [TearDown]. To improve test isolation, it is recommended that context is not shared between tests. If you still want to share context between tests, then you can do so in two different ways.
- Create your objects in the constructor along with implementing IDisposable, using the dispose method to clean anything up. This is particularly useful if you want shared setup and clean-up code without sharing object instances.
- Use class fixtures. This is achieved by implementing IClassFixture. This is used for shared object instances across tests in a single file. Once the tests finish executing the fixture is cleaned up.
In more complex scenarios, developers often create fixtures several times per file catering the needs of their test suites, however using the above example ensures that fixture is created once per test file. This is extremely useful for when the creation of the fixture is expensive.
These are some of the main differences that we found moving from NUnit, their documentation is a great resource to begin with.
Moq was chosen as the mocking framework for its simplicity, providing a clean api for mocking and verification. AutoFixture supports it out of the box and its simplicity means it has a very fast learning curve.
To get started with Moq run install-package moq in the package manager console.
The following example shows a product service with one dependency of IDatabase. IDatabase is used for interacting with a data store via commands and queries.
To test this class I have mocked the interactions with the data store. Moq allows you to parameter match using the It class. This allows us to assert that our system under test is calling out to its dependencies in the way we expect, with the parameters we expect.
This is a very brief example of how we can use Moq for setup and verification. Further examples of its capabilities can be found in their documentation.
AutoFixture is a library primarily written by Mark Seemann along with many other contributors. It allows developers to be more productive when writing unit tests, allowing them to concentrate less on the ‘arrange phase’ and making sure that their unit tests are refactoring-safe. It manages all of this this in a few ways.
- Auto mocking of dependencies and object graphs.
- Test data generators.
Let’s rewrite the tests we wrote for the Product Service using AutoFixture with Moq. First run install-package autofixture.AutoMoq.
In the example above we use the fixture object in the setup phase of our tests. When configured with Moq, AutoFixture is turned into an auto mocking container. We can use it to mock implementations of interfaces and then “freeze” them. This will ensure that when this dependency is required in the object graph within that tests scope, the frozen dependency will be used.
The product and product list objects are also created using the fixture. This saves time as these objects are returned with auto generated data. It also makes the tests more readable as less code is written setting up the data.
Creating the system under test using the fixture means any changes to the signature of the Product Services constructor will not cause any tests files to break. Any extra dependencies we add to that service will be automatically mocked out by AutoFixture.
We can make sure the setup phase is even simpler by using auto mocking attributes. These wrap the functionality of AutoFixture into an attribute that we then use to decorate our tests. First install AutoFixture.Xunit2 using install-package autofixture.xunit2. Add the following class to your test project.
An example of the When_AddProductCalled_IDatabaseCalledWithCorrectParameters test using the above AutoMoqDataAttribute would look like this.
As you can see, there is no setup required, everything we need for this unit test is passed into the test method as parameters. AutoFixture, using Moq, creates all of our data objects we need along with the system under test and all its dependencies. If we need to setup or verify a dependency, the [Frozen] attribute is used. This ensures that the dependency we have frozen, in this case Mock<IDatabase>, is the object injected into the ProductService class.
A rewrite of the When_GetAllProductsUnderPriceCalled_AllProductsReturned test, using testing attributes, would look like this.
In this example I have shown you how you can use the InlineAutoMoqDataAttribute to run the same test multiple times, with different parameters whilst still making use of AutoFixtures auto mocking capabilities. To do this you need to use this attribute.
I hope this brief introduction has been helpful in showing how we have applied these technologies to our test suites and how they have improved our unit testing process. All source code in this article can be found on Github.