Introduction
Flexible and expressive unit tests with the builder pattern
.NET

Flexible and expressive unit tests with the builder pattern

Unit tests become more valuable if they are expressive and flexible. A good unit test should not only test your code, but also document it. Creating expressive and flexible unit tests is not always easy however. The reason of this is that there’s an inherit discrepancy between the goals of the client of your code in production and the client of your code in test. In a production environment you want encapsulation, constraints and other limitations. Under test, however, you want to expose units, so that you can test them in isolation. Let’s take a look at a small example where these goals enter in conflict.

public class Employee { public Employee(int id, string firstname, string lastname, DateTime birthdate, string street) { this.ID = id; this.FirstName = firstname; this.LastName = lastname; this.BirthDate = birthdate; this.Street = street; } public int ID { get; private set; } public string FirstName { get; private set; } public string LastName { get; private set; } public DateTime BirthDate { get; private set; } public string Street { get; private set; } public string getFullName() { return this.FirstName + " " + this.LastName; } public int getAge() { DateTime today = DateTime.Today; int age = today.Year - BirthDate.Year; if (BirthDate > today.AddYears(-age)) age--; return age; } }

This is a (simplified) version of a typical domain class. It has the following characteristics:

  • All values are passed in in the constructor
  • All properties are read-only (at least from outside of this class)
  • Methods provide access to calculated values (getFullName and getAge)

This is what is known as an immutable class.

Now let’s look at a simple unit test for the getFullName-method:

public class EmployeeTest { [Test] public void GetFullNameReturnsCombination() { // Arrange Employee emp = new Employee(1, "Kenneth", "Truyers", new DateTime(1970, 1, 1), "My Street"); // Act string fullname = emp.getFullName(); // Assert Assert.That(fullname, Is.EqualTo("Kenneth Truyers")); } }

This simple test constructs an employee, calls the method and then checks that the returned value is correct. The problem here is that we’re tied to the constructor. In order to construct the employee, apart from the relevant data (firstname and lastname) we also need to pass in an ID, a birth date and a street. This data is completely irrelevant for this test. This has two consequences:

  • Our test is not as expressive as it could be. The irrelevant data pollutes our test. From just looking at the test, it’s not clear whether they influence the outcome our not. (In this example it might be obvious, but in a real situation that’s not always the case.
  • If at any given point in time, we need to add an unrelated item to the constructor, it will break the test and we need to modify it. This is not a big problem for a single test, but it can become a maintenance nightmare if you have to construct Employee-objects in a lot of different unit tests.

You can see the problem with two different goals here:

  • For our production code, we want the Employee-class to be immutable. This way the Employee-class can encapsulate its data and assure that operations work on the correct data that has not been tampered with.
  • For our unit test however, we would like to be able to only provide some data so we can test the relevant methods.

One route to take would be to create an overloaded constructor on the Employee-class and establish a convention that always the full-fledged constructor should be used. This is not optimal however, since you will end up with a lot of conventions, which are then forgotten and it can affect your production code. This is exactly the opposite of what we want to achieve with unit tests.

The Builder pattern

In essence, the problem we’re facing is that our unit test is bound to the constructor. A common design pattern to resolve this dependency is the builder pattern. This is the definition of the builder pattern from the book Design Patterns: Elements of Reusable Object-Oriented Software:

The builder pattern separates the construction of a complex object from its representation so that the same construction process can create different representations

Let’s implement this pattern step-by-step:

Step 1: Create a class with a method that creates an Employee.

This step  is fairly simple.This will be our** replacement for the direct constructor** call we had in our unit test. It constructs an employee based on some private fields.

public class EmployeeBuilder { private int id = 1; private string firstname = "first"; private string lastname = "last"; private DateTime birthdate = DateTime.Today; private string street = "street"; public Employee Build() { return new Employee(id, firstname, lastname, birthdate, street); } }

Step 2: Create modification methods

In order for our builder to be useful, we need to be able to modify the values of these private fields. We can do this by implementing some modification methods:

public void WithFirstName(string firstname) { this.firstname = firstname; } public void WithLastName(string lastname) { this.lastname = lastname; }

With these methods in place you can now modify the first and lastname before constructing the object. This is an example of the usage:

EmployeeBuilder builder = new EmployeeBuilder(); builder.WithFirstName("Kenneth"); builder.WithLastName("Truyers"); Employee emp = builder.Build();

As you can see in this example, we have now decoupled the construction from the constructor and provided an API for constructing employee-objects. However, we can still improve on the pattern. The following two steps are optional, but since they don’t require a lot of work I would recommend implementing them as well since it vastly improves the expressiveness of your tests.

Step 3: Create a fluent API

In order to make our client code a bit more concise, we can implement a fluent API. We can do this by modifying the modifier-methods:

public EmployeeBuilder WithFirstName(string firstname) { this.firstname = firstname; return this; } public EmployeeBuilder WithLastName(string lastname) { this.lastname = lastname; return this; }

As you can see, instead of returning void, all methods now return the current instance. This allows us to **chain **the methods and rewrite our client code to this:

EmployeeBuilder builder = new EmployeeBuilder().WithFirstName("Kenneth") .WithLastName("Truyers"); Employee emp = builder.Build();

Step 4: Provide automatic conversion

After our last step, we can still make the code a bit more concise by using a c# feature called operator overloading. It let’s you define some code you can execute when the runtime tries to convert a type to another type. In our case, we would like to convert an EmployeeBuilder to an Employee. The operator overload can be implemented like this:

public static implicit operator Employee(EmployeeBuilder instance) { return instance.Build(); }

This is a static method that receives an instance of an EmployeeBuilder and returns an Employee. In this case we perform the conversion by calling the Build-method on the builder, thereby returning the constructed Employee. The implicit keyword tells the compiler that an explicit cast is not required. This let’s us write our client code like this:

Employee emp = new EmployeeBuilder().WithFirstName("Kenneth") .WithLastName("Truyers");

The completed builder

The following listing shows the completed EmployeeBuilder class with the implementation of the two remaining modification methods:

public class EmployeeBuilder { private int id = 1; private string firstname = "first"; private string lastname = "last"; private DateTime birthdate = DateTime.Today; private string street = "street"; public Employee Build() { return new Employee(id, firstname, lastname, birthdate, street); } public EmployeeBuilder WithFirstName(string firstname) { this.firstname = firstname; return this; } public EmployeeBuilder WithLastName(string lastname) { this.lastname = lastname; return this; } public EmployeeBuilder WithBirthDate(DateTime birthdate) { this.birthdate = birthdate; return this; } public EmployeeBuilder WithStreet(string street) { this.street = street; return this; } public static implicit operator Employee(EmployeeBuilder instance) { return instance.Build(); } }

The resulting unit tests

With our EmployeeBuilder in place we can now rewrite our unit tests. The following listing shows the rewritten test as well as a unit test for the getAge-method:

public class EmployeeTest { [Test] public void GetFullNameReturnsCombination() { // Arrange Employee emp = new EmployeeBuilder().WithFirstName("Kenneth") .WithLastName("Truyers"); // Act string fullname = emp.getFullName(); // Assert Assert.That(fullname, Is.EqualTo("Kenneth Truyers")); } [Test] public void GetAgeReturnsCorrectValue() { // Arrange Employee emp = new EmployeeBuilder().WithBirthDate(new DateTime(1983, 1,1)); // Act int age = emp.getAge(); // Assert Assert.That(age, Is.EqualTo(DateTime.Today.Year - 1983)); } }

The rewritten tests are now more expressive. Only relevant data for that particular test is present in the test. Apart from that, our unit tests are now more flexible in light of changes. When the constructor changes, we only need to modify the builder, our existing tests will remain untouched.

Note: The Builder is part of your test suite. In general, it should live inside your test-project, not inside the production code. (unless your general pattern is to mix production and test code, but that’s an entirely different discussion).

Conclusion

Using the Builder-pattern can bring major improvements to your unit tests:

  • Expressiveness: By explicitly passing just the necessary data we improve the value of our unit tests as a form of documentation. Just by looking at the test you can determine what the method does.
  • Flexibility: By decoupling the test from the constructor, we made sure that future changes won’t break our existing unit tests. This is important for maintenance reasons (you don’t want to go in and change a lot of tests, because of some code changes).
  • Reliability: Because our unit test is flexible towards changes, you won’t have to modify it often. In general, a unit test gets more reliable when it matures. To imagine this, you can compare the effect of a failing unit test that you just wrote over one that was written months ago. A recently written unit test that fails can have a lot of reasons (an error in the test, some code that is not written yet, etc.). On the other hand, a test that has been working for months but suddenly fails is more concerning and will most definitely indicate a problem with new code.

I have used the Builder-pattern in various projects and found it very useful in a lot of different circumstances. I hope I have provided a good enough explanation so you can leverage the power of this pattern as well.

Kenneth Truyers
View Comments
Next Post

Automated acceptance testing with Cucumber for .NET and JAVA

Previous Post

A brief comparison of fundamental AOP techniques