Over the past 20 years, testing has become a cornerstone of software development. To turn software development into a true profession, we need to create robust, secure, safe, and efficient systems. The simplest tool in our toolbox starts us on this road: the unit test.

Like an accountant, the unit test is a fantastic way for us to perform double-entry bookkeeping. The unit test double checks our work is correct, and makes sure we keep it from morphing into something we did not want. However, unit tests, without proper maintenance and care, can become unwieldly. We must take great care in unit tests, as much as we do with source code itself.

This article will go into two simple refactorings that will help you improve your unit tests: the builder, and the fixture. These two techniques will make your unit tests more maintainable and less brittle to source code change.

When designing a unit test, I like to use the famous Arrange, Act, Assert method. As people who use this technique may know, the arrange phase can balloon in size. With complex unit tests, the arrange phase can be 90% of your unit test. The builder and fixture patterns try and reduce this, simplifying your unit tests, and encouraging the don’t repeat yourself principle.

N.B. Examples shown will be using C# with xUnit. However, ideas can be applied to any object-oriented language.

The Builder

The builder pattern can reduce the arrange phase considerably. Let’s take an example piece of code:

public class RazorProjectPageRouteModelProvider : IPageRouteModelProvider { private readonly RazorProject _project; private readonly RazorPagesOptions _pagesOptions; private readonly ILogger _logger; public RazorProjectPageRouteModelProvider( RazorProject razorProject, IOptions<RazorPagesOptions> pagesOptionsAccessor, ILoggerFactory loggerFactory) { _project = razorProject; _pagesOptions = pagesOptionsAccessor.Value; _logger = loggerFactory.CreateLogger<RazorProjectPageRouteModelProvider>(); } ... }

This is an excerpt from the ASP.NET Core MVC GitHub repository. It show a class that has three dependencies: RazorProject, IOptions{T}, and an ILogger.

If we want to unit test the class, then we need to arrange it:

[Fact] public void OnProvidersExecuting_ReturnsPagesWithPageDirective() { // Arrange var fileProvider = new TestFileProvider(); var file1 = fileProvider.AddFile("/Pages/Home.cshtml", "@page"); var file2 = fileProvider.AddFile("/Pages/Test.cshtml", "Hello world"); var dir1 = fileProvider.AddDirectoryContent("/Pages", new IFileInfo[] { file1, file2 }); fileProvider.AddDirectoryContent("/", new[] { dir1 }); var project = new TestRazorProject(fileProvider); var optionsManager = new TestOptionsManager<RazorPagesOptions>(); optionsManager.Value.RootDirectory = "/"; var provider = new RazorProjectPageRouteModelProvider(project, optionsManager, NullLoggerFactory.Instance); var context = new PageRouteModelProviderContext(); // Act & Assert ... }

This is an example test from the ASP.NET Core repository that tests the provider class. As can be predicted, the arrange phase is the biggest part of this unit test. So how can we improve this code?

One part of this code is the arrangement of the TestRazorProject class. To make this code more readable we can introduce a TestRazorProjectBuilder:

public class TestRazorProjectBuilder { private readonly TestFileProvider fileProvider = new TestFileProvider(); private readonly List<IFileInfo> files = new List<IFileInfo>(); public TestRazorProjectBuilder AddFile(string path, string name) { var file = this.fileProvider.AddFile(path, name); this.files.Add(file); return this; } public TestRazorProject Build() { var dir1 = this.fileProvider.AddDirectoryContent( "/Pages", this.files.ToArray()); fileProvider.AddDirectoryContent("/", new[] { dir1 }); return new TestRazorProject(this.fileProvider); } public static implicit operator TestRazorProject( TestRazorProjectBuilder builder) => builder.Build(); }

What we have done here is taken the code that is directly in the unit test, and moved it into its own encapsulation.

There are two important points to make about this builder class. Firstly, The AddFile method is using a fluent interface syntax. This means it is returning the current instance of the builder, so calls to methods on the builder can be chained. Secondly, we are doing an implicit cast to the TestRazorProject object. These two points allow us to refactor the unit test into the following (note the implicit cast):

[Fact] public void OnProvidersExecuting_ReturnsPagesWithPageDirective() { // Arrange TestRazorProject project = new TestRazorProjectBuilder() .AddFile("/Pages/Home.cshtml", "@page") .AddFile("/Pages/Test.cshtml", "Hello world"); var optionsManager = new TestOptionsManager<RazorPagesOptions>(); optionsManager.Value.RootDirectory = "/"; var provider = new RazorProjectPageRouteModelProvider(project, optionsManager, NullLoggerFactory.Instance); var context = new PageRouteModelProviderContext(); // Act & Assert ... }

The arrange code is now 8 lines, down from 13. Not a massive jump, but I hope you see, it is a lot more readable, and the code inside the builder is now reusable. We could extend the builder further, and allow it to create custom directory hierarchies.

The builder pattern is just the starting point in reducing the arrange phase. The fixture can help the unit test to become even more succinct.

The Fixture

A fixture is a class which creates the system under test. In our example, the system under test is the RazorProjectPageRouteModelProvider. An example fixture for the provider could be:

public class RazorProjectPageRouteModelProviderFixture { private readonly TestRazorProjectBuilder projectBuilder = new TestRazorProjectBuilder(); private readonly TestOptionsManager<RazorPagesOptions> optionsManager = new TestOptionsManager<RazorPagesOptions>(); public RazorProjectPageRouteModelProviderFixture() { this.optionsManager.Value.RootDirectory = "/"; } public RazorProjectPageRouteModelProviderFixture SetRootDirectory(string root) { this.optionsManager.Value.RootDirectory = root; return this; } public RazorProjectPageRouteModelProviderFixture AddFile(string path, string file) { this.projectBuilder.AddFile(path, file); return this; } public RazorProjectPageRouteModelProvider CreateSut() { return new RazorProjectPageRouteModelProvider( this.projectBuilder.Build(), this.optionsManager, NullLoggerFactory.Instance); } }

In a similar way to the builder pattern, we are providing a fluent interface in order to allow chaining of method calls to the fixture. On construction, we are providing sensible defaults, but we also allow the setting of these through method calls to the fixture.

The final unit test can now look like this:

[Fact] public void OnProvidersExecuting_ReturnsPagesWithPageDirective() { // Arrange var sut = new RazorProjectPageRouteModelProviderFixture() .AddFile("/Pages/Home.cshtml", "@page") .AddFile("/Pages/Test.cshtml", "Hello world") .CreateSut(); var context = new PageRouteModelProviderContext(); // Act & Assert ... }

This arrange phase is now simple and succinct. This will lead to code reuse between unit tests, and an easier maintenance overhead.

The most important benefit of a fixture class, is that if we change how we instantiate the system under test, and we have 100 unit tests using the fixture, only the fixture has to change. This is easier than modifying all 100 unit tests due to adding a dependency to our system under test.

Summary

What we have learned in this article is the application of the builder and fixture patterns. The builder pattern allows us to make unit tests easier to read, and can help in instantiating complex auxiliary classes. The fixture pattern allows us to encapsulate the way we create systems under test, and therefore reduces the overhead when changes to the source code occur.

There are tools out there that can make our lives even easier in the arrange phase. A popular tool that I encourage you to investigate (and I may write a blog post for in the future) is AutoFixture. AutoFixture provides a way to remove your arrange phase completely.

For more helpful ways of refactoring your unit tests, I recommend reading xUnit Test Patterns. It is an in-depth look at how we can refactor unit tests to make them more robust and maintainable.

I hope this helps.