It’s rather easy to find information about how to write a good unit test in the means of the test itself. But how should we handle the data that we need to test? How can we create test data structures in a flexible way?
As a DDD (Domain-Driven Development) person, I like to be able to read the code as I read a book. This is of course very hard, but something that I strive for. Therefore I often use the builder pattern for unit tests.
Builder pattern
Generally, a good approach is to create the data within the test itself. This makes it easy to see exactly what the test do. But after some tests you often find the same code repeated many times so you start looking for a generic way of generating the data suitable for each little thing you want to test. The builder pattern comes in handy here.
Let’s say you want to test the following test case:
* When you cancel an order, it gets the status cancelled. *
The test
You need to create an order with the status InProgress. Your test will then change the order to Canceled an verify that it worked. In a real system, you would probably have a lot of tests around status changes. Therefore it would be suitable to have code that creates an order in different statuses.
With the builder pattern to generate the order the test might look something like this:
[Fact]
public void CancelOrderTest()
{
//Arrange
var order = new OrderBuilder()
.WithSingle()
.WithStatusInProgress()
.Build()
.First();
//Act
order.Cancel();
//Assert
order.Status.Should().Be(OrderStatus.Canceled);
}
Let’s start with the “Arrange” part. A new OrderBuilder is created and asked to create a single order with the status InProgress. When looking into the builder later on you’ll see why the .Build() and .First() methods are needed.
The “Act” part here is intended to cancel the order, and the “Assert” checks whether the order has got the status Canceled. The FluentAssertions package is used to get a nice and fluent syntax of the assertions.
The builder
The OrderBuilder is a class with methods that returns the class itself. This is what makes it possible to use the fluent syntax in the test. Only the Build() method returns the actual list of orders:
public class OrderBuilder
{
private List<Order> _orders = new List<Order>();
public OrderBuilder WithSingle()
{
_orders.Add(new Order());
return this;
}
public OrderBuilder WithStatusInProgress()
{
_orders.Last().StartProcessing();
return this;
}
public List<Order> Build()
{
return _orders;
}
}
I chose to prepare the builder to handle many orders, by adding a private list variable in the top of it. Of course that could also be a single order.
The WithSingle and WithStatusInProgress methods both operates on the private class variable and step by step build up the order that the caller of the builder wants to have. More methods can be added and freely combined and called after each other to e.g. build up a list of orders:
//Arrange
var orders = new OrderBuilder()
.WithSingle().WithStatusInProgress()
.WithSingle().WithStatusNew()
.Build();
The Build method returns the orders and breaks the chain by not returning the TestBuilder class.
A functional approach
Another approach to the problem is to use a functional way of building up the test data. Sometimes you have a lot of properties, and want to change them in a controlled way to get exact the data you want to test. You can add a method to the builder that lets you create an order this way:
[Fact]
public void CancelOrderTest2()
{
//Arrange
var order = new OrderBuilder()
.WithSingle(o => o.Status = OrderStatus.InProgress)
.Build()
.First();
//Act
order.Cancel();
//Assert
order.Status.Should().Be(OrderStatus.Canceled);
}
You can from the test set which properties you want to change on the order, and combine with other builder methods if you want. The WithSingle builder method that takes a function parameter looks like this:
internal OrderBuilder WithSingle(Action<Order> action)
{
_orders.Add(new Order());
action?.Invoke(_orders.Last());
return this;
}
It takes a function as an argument, creates an order and applies the function to it.
Conclusion
The builder pattern has helped me a lot and it is always fun to use it! I show it here as I learned it from start, but in every project I use it in a special flavor suitable exactly for that special environment.
Hi Christina.
Very interesting reading.
I think a typo exists here:
“The Build method returns the orders and breaks the chain by not returning the TestBuilder class.”
should be
“The Build method returns the orders and breaks the chain by not returning the OrderBuilder class.” ?