Photo by Vitolda Klein
I tend to use the builder pattern quite a bit when writing my tests. It's really convinient to create a builder and have a fluent interface when creating some test data.
We'll be using the following Person class in our examples:
public class Person
{
public Person(string givenName, string familyName, uint age)
{
GivenName = givenName;
FamilyName = familyName;
Age = age;
}
public string GivenName { get; }
public string FamilyName { get; }
public int Age { get; }
}
Let's add a test to verify that we can create a Person.
[Fact]
public void CanCreatePerson()
{
var givenName = "Jane";
var familyName = "Doe";
var age = 33;
var person = new Person(givenName, familyName, age);
person.GivenName.ShouldBe(givenName);
person.FamilyName.ShouldBe(familyName);
person.Age.ShouldBe(age);
}
Now, imagine that we want to create a bunch of different Persons in a test for some reason. Wouldn't it be a bit tedious to do it like this?
[Fact]
public void CanCreatePerson_Multiple()
{
var givenName1 = "Jane";
var familyName1 = "Doe";
var age1 = 33;
var givenName2 = "John";
var familyName2 = "Doe";
var age2 = 40;
var givenName3 = "Albert";
var familyName3 = "Doe";
var age3 = 18;
var person1 = new Person(givenName1, familyName1, age1);
var person2 = new Person(givenName2, familyName2, age2);
var person3 = new Person(givenName3, familyName3, age3);
person1.GivenName.ShouldBe(givenName1);
person1.FamilyName.ShouldBe(familyName1);
person1.Age.ShouldBe(age1);
person2.GivenName.ShouldBe(givenName2);
person2.FamilyName.ShouldBe(familyName2);
person2.Age.ShouldBe(age2);
person3.GivenName.ShouldBe(givenName3);
person3.FamilyName.ShouldBe(familyName3);
person3.Age.ShouldBe(age3);
}
There's a couple of "problems" with the above approach:
- The code is a bit hard to read because we are repeating ourselves in the arrange phase.
- It's error prone. It's really easy to mix up the variables.
- We need to explicitly set all properties on the Person object. More on this later.
- It looks ugly :)
Let's clean it up!
Builder pattern
This is our first implementation of a builder.
internal class PersonBuilder
{
private string _givenName;
private string _familyName;
private int _age;
internal PersonBuilder()
{
_givenName = "Jane";
_familyName = "Doe";
_age = Random.Shared.Next(18, 101);
}
internal PersonBuilder WithGivenName(string givenName)
{
_givenName = givenName;
return this;
}
internal PersonBuilder WithFamilyName(string familyName)
{
_familyName = familyName;
return this;
}
internal PersonBuilder WithAge(int age)
{
_age = age;
return this;
}
internal Person Build()
{
return new Person(_givenName, _familyName, _age);
}
}
The first test we wrote can be refactored like this (we're now using the builder).
[Fact]
public void CanCreatePerson_Builder()
{
var givenName = "Jane";
var familyName = "Doe";
var age = 33;
var person = new PersonBuilder()
.WithGivenName(givenName)
.WithFamilyName(familyName)
.WithAge(age)
.Build();
person.GivenName.ShouldBe(givenName);
person.FamilyName.ShouldBe(familyName);
person.Age.ShouldBe(age);
}
Cool, we are now using our builder. But...we haven't really achieved anything? We've just added more code to our test?
Let's refactor the second test as well.
[Fact]
public void CanCreatePerson_Builder_Multiple()
{
var givenName1 = "Jane";
var familyName1 = "Doe";
var age1 = 33;
var givenName2 = "John";
var familyName2 = "Doe";
var age2 = 40;
var givenName3 = "Albert";
var familyName3 = "Doe";
var age3 = 18;
var person1 = new PersonBuilder()
.WithGivenName(givenName1)
.WithFamilyName(familyName1)
.WithAge(age1)
.Build();
var person2 = new PersonBuilder()
.WithGivenName(givenName2)
.WithFamilyName(familyName2)
.WithAge(age2)
.Build();
var person3 = new PersonBuilder()
.WithGivenName(givenName3)
.WithFamilyName(familyName3)
.WithAge(age3)
.Build();
person1.GivenName.ShouldBe(givenName1);
person1.FamilyName.ShouldBe(familyName1);
person1.Age.ShouldBe(age1);
person2.GivenName.ShouldBe(givenName2);
person2.FamilyName.ShouldBe(familyName2);
person2.Age.ShouldBe(age2);
person3.GivenName.ShouldBe(givenName3);
person3.FamilyName.ShouldBe(familyName3);
person3.Age.ShouldBe(age3);
}
Great, even more code.
So yeah, if we really need to specify all values when creating a person, the builder pattern isn't really helping us. In fact, it just adds more code.
The above tests are not that realistic though. Usually when you are arranging your objects in your tests, you might not care about all values on your object. You might only be interested in creating a Person so that you can pass it into your sut for example. Or maybe you only care about the GivenName for example.
Let's write a test that creates 3 persons and then verifies that they have correct given names.
[Fact]
public void GivenNameGetsSetCorrectly()
{
var givenName1 = "Jane";
var givenName2 = "John";
var givenName3 = "Albert";
var person1 = new PersonBuilder().WithGivenName(givenName1).Build();
var person2 = new PersonBuilder().WithGivenName(givenName2).Build();
var person3 = new PersonBuilder().WithGivenName(givenName3).Build();
person1.GivenName.ShouldBe(givenName1);
person2.GivenName.ShouldBe(givenName2);
person3.GivenName.ShouldBe(givenName3);
}
Here we can see one of the strengths of this pattern. We only care about the given name in this test, so we are explicitly setting the given name and then we leave it to the builder to create a valid Person object. Nice.
What if we don't care about the values at all? We just want to create some persons?
[Fact]
public void CanCreateMultiplePersons()
{
var person1 = new PersonBuilder().Build();
var person2 = new PersonBuilder().Build();
var person3 = new PersonBuilder().Build();
person1.GivenName.ShouldNotBeNull();
person2.GivenName.ShouldNotBeNull();
person3.GivenName.ShouldNotBeNull();
}
New properties
Another great thing about the builder pattern is how new properties are handled. Let's add a new height property to our Person.
public class Person
{
public Person(string givenName, string familyName, int age, int height)
{
GivenName = givenName;
FamilyName = familyName;
Age = age;
Height = height;
}
public string GivenName { get; }
public string FamilyName { get; }
public int Age { get; }
public int Height { get; }
}
Now we will get a bunch of compilation errors in all of our tests that doesn't use our builder. So now we need to go through each and everyone of them and correct this. This is boring and can also be a bit "dangerous" since we, most likely, will just copy-paste the same value for all the tests.
So by using the builder, we can keep all changes related to the creation of the Person object in 1 place, meaning we only need to update our code once.
We will also need to create a new method in our builder so that we can set the height. Our builder now looks like this.
internal class PersonBuilder
{
private string _givenName;
private string _familyName;
private int _age;
private int _height;
internal PersonBuilder()
{
_givenName = "Jane";
_familyName = "Doe";
_age = Random.Shared.Next(18, 101);
_height = Random.Shared.Next(130, 201);
}
internal PersonBuilder WithGivenName(string givenName)
{
_givenName = givenName;
return this;
}
internal PersonBuilder WithFamilyName(string familyName)
{
_familyName = familyName;
return this;
}
internal PersonBuilder WithAge(int age)
{
_age = age;
return this;
}
internal PersonBuilder WithHeight(int height)
{
_height = height;
return this;
}
internal Person Build()
{
return new Person(_givenName, _familyName, _age, _height);
}
}
And that's why I wanted to write this post. IMO, it's boring to write this WithXXX code. Yes, you only write it once but still. It's easy to forget and yada yada. Luckily, the c# language can help us here!
Builder pattern - record
By using a record instead of a class, we no longer need to create all the WithXXX methods since they will be autogenerated for us!
Records support with expressions to enable non-destructive mutation of records.
internal record PersonBuilderRecord
{
internal PersonBuilderRecord()
{
GivenName = "Jane";
FamilyName = "Doe";
Age = Random.Shared.Next(18, 101);
Height = Random.Shared.Next(130, 201);
}
internal string GivenName { get; init; }
internal string FamilyName { get; init; }
internal int Age { get; init; }
internal int Height { get; init; }
internal Person Build()
{
return new Person(GivenName, FamilyName, Age, Height);
}
}
Also, by using the init
keyword, we can use our builder like this (this works for non-records as well):
[Fact]
public void GivenNameGetsSetCorrectly_RecordBuilder()
{
var givenName1 = "Jane";
var givenName2 = "John";
var givenName3 = "Albert";
var person1 = new PersonBuilderRecord {GivenName = givenName1}.Build();
var person2 = new PersonBuilderRecord {GivenName = givenName2}.Build();
var person3 = new PersonBuilderRecord {GivenName = givenName3}.Build();
person1.GivenName.ShouldBe(givenName1);
person2.GivenName.ShouldBe(givenName2);
person3.GivenName.ShouldBe(givenName3);
}
Pretty sweet right?
Now, say that we want to create an exact copy of person1, but the only thing that should differ should be the FamilyName. How can we achieve that? This is where records shine!
[Fact]
public void FamilyNameGetsSetCorrectly_RecordBuilder()
{
var familyName1 = "Doe";
var familyName2 = "Ottosson";
var person1Builder = new PersonBuilderRecord { FamilyName = familyName1 };
var person1 = person1Builder.Build();
var person2 = (person1Builder with { FamilyName = familyName2 }).Build();
person1.FamilyName.ShouldBe(familyName1);
person2.FamilyName.ShouldBe(familyName2);
person2.Height.ShouldBe(person1.Height);
person2.GivenName.ShouldBe(person1.GivenName);
person2.Age.ShouldBe(person1.Age);
}
Here we're using the with
keyword when creating our person2
. What this does is basically "create a copy of person1Builder
but use the provided FamilyName
".