Apply Builder Pattern to Unit Tests

(Tech: C#)

Problem

I was working on a team (Team-B) where we were tasked with writing tests for a new implementation of the codebase.
The old implementation was fully covered, and our mission was to mimic the old tests in the new ones.

Sounds fairly simple, butttt…

The codebase is owned by another team (Team-A), and they have not contributed to the new implementation.

How do we create unit tests which will speak to Team-A when they read the new tests from the imposters? :)

Use the Builder Pattern

Here’s wikipedia’s definition on the builder pattern:

The intent of the Builder design pattern is to separate the construction of a complex object from its representation.
By doing so the same construction process can create different representations.

Because Team-A doesn’t have the knowledge of creating valid data objects for the new implementation, we used the builder pattern and created methods to abstract away the complex data structure setup.

Think of it from this point, if someone else wants to add more tests they should not have to dig deep into business logic on how to create a valid object. There should be a method they can call, and they will get a valid object every time.

One advantage I found was; the longer we worked on the test suite, the faster we got. This was because we started re-using builders created by other team members.

When I wanted to get a complex object in a valid state before I focus on my test, I’ll simply “new up” a builder and look for some interesting method on it. If not found, I knew it was a new scenario and method to be created.

Let’s look at some samples.

Samples

Disclaimer: These are all fictional - apply the principle and don’t get too bogged down in the example :)

(Samples github repo)

Let’s say another team is tasked with creating an Employee validator.
This team has no idea on how to create a valid employee.
Luckily another awesome team has created EmployeeBuilder, which they can re-use and focus on the logic of testing the validator.

Here is a requirement: An employee is valid when any address has an Australian postcode

Let’s dream up some test code for the employee validator…

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
[Fact]
public void EmployeeValidatorTests_EmployeeAddressShouldBeVALIDWhenAnyPostCodeFromAustralia()
{
// arrange
var builder = new EmployeeBuilder();

// no need to worry on how an Australian employee is created.
// imagine a very complex object here, with a lot of setup under the "With" method.
var employee = builder.WithEmployeeFromAustralia()
.Build();

// act
//system under test
var sut = new EmployeeValidator(employee);

// assert
// team focus on testing validator logic,
// not spending time figuring out how to create a complex employee object.
sut.IsValidAustralianAddress().ShouldBeTrue();
}

Let’s check out the EmployeeBuilder class.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
public class EmployeeBuilder : BuilderBase<Employee, EmployeeBuilder>
{
AddressBuilder _addressBuilder = new AddressBuilder();

public EmployeeBuilder WithEmployeeFromAustralia()
{
_concreteObject = new Employee()
{
Name = "Bruce",
LastName = "Ozzy",

Addresses = new List<Address>
{
// default values
_addressBuilder.WithAustralianAddress().Build(),
}
};

return this;
}
}

public class BuilderBase<TBuildResult, TBuilder> : IBuilder<TBuildResult, TBuilder>
where TBuildResult : class, new()
where TBuilder : class, IBuilder
{
protected TBuildResult _concreteObject = new TBuildResult();

public TBuildResult Build()
{
return _concreteObject;
}

public TBuilder With(Action<TBuildResult> setAction)
{
setAction?.Invoke(_concreteObject);
return this as TBuilder;
}

public TBuilder With<TRequestBuilder>(Action<TBuildResult, TRequestBuilder> setAction) where TRequestBuilder: class, IBuilder, new()
{
setAction?.Invoke(_concreteObject, new TRequestBuilder());
return this as TBuilder;
}
}

public interface IBuilder { /* maker to indicate a builder object */ }

public interface IBuilder<TBuildResult, TBuilder> : IBuilder
where TBuildResult : class, new()
where TBuilder : class, IBuilder
{
TBuildResult Build();

/// <summary>
/// A generic way to set properties
/// </summary>
TBuilder With(Action<TBuildResult> setAction);

TBuilder With<TRequestBuilder>(Action<TBuildResult, TRequestBuilder> setAction) where TRequestBuilder : class, IBuilder, new();
}

You’ll notice I have TBuilder With(Action<TBuildResult> setAction) on the BuilderBase class.

It allows me code like this:

1
2
3
4
var builder = new EmployeeBuilder();

var actual = builder.With(x => x.Name = "Samurai Jack")
.Build();

I like it because one can easily see from the test what the intent is.
Abstracting this away into a method will hide the fact that the Name property changed.

There is also
TBuilder With<TRequestBuilder>(Action<TBuildResult, TRequestBuilder> setAction) where TRequestBuilder : class, IBuilder, new()
on the BuilderBase class.

It allows me code like this:

1
2
3
4
5
6
7
8
var builder = new EmployeeBuilder();
var employee = builder
.WithEmployeeFromAustralia()
.With<AddressBuilder>((e, addressBuilder) => e.Addresses.Add(addressBuilder
.WithSouthAfricanAddress()
.Build())
)
.Build();

My suggestion would be to have all builder classes only in your test project.
No need to have builders in production code as the data will come from the real source.

More samples can be found on my github repo.

(Btw, I’m using Shouldly for my assertions)

TestStack.Dossier - Check it out!

This is more in-depth “framework” and something I will definitely use on bigger projects.
If I needed more complex logic in my current implementation of the BuilderBase, I’ll upgrade to Dossier no questions asked.

TestStack.Dossier provides you with the code infrastructure to easily and quickly generate test fixture data for your automated tests in a terse, readable and maintainable way using the Test Data Builder, anonymous value and equivalence class patterns.

Conclusion

All the logic of creating complex objects is kept in one place, will help with future maintenance.
Writing tests in a fluent way makes the test more readable and also shows testing intent better.
Writing tests got faster as the builders library grew.
Cross functional teams don’t need the in-depth knowledge of a specific area, if there is a builder they can focus on their tasks.

I had fun using the builder pattern to create data objects in a fluent(ish) way for unit testing.
Will use it again in future.

Use it…don’t use it :)