Builder Pattern: Good for code, great for tests
I’ve found the builder design pattern occasionally useful in code, but frequently useful in tests. This article is a quick summary of the pattern in general, followed by look at a working example of using it in tests. See the code in github.
Background to the Builder pattern
According to the GoF book, the builder design pattern is used to “Separate the construction of a complex object from its representation so that the same construction process can create different representations”. Like most of the GoF book, that is an accurate if dull description.
Josh Bloch, in his Effective Java book, suggests a more interesting use for builders. The problem his approach was trying to solve is when a class has “more than a handful” of parameters that would normally be set via a constructor, and many of them may be optional. Typical solutions are
- A telescoping constructor pattern, in which you provide a constructor with only the required parameters, as well as additional constructors with variations of the optional parameters, culminating in a constructor with all the optional parameters.
This works, but makes for a fairly messy solution that has a potentially large number of constructors to cover all permutations - A simpler constructor (e.g. for just the required parameters), backed up by setter methods for the optional parameters (the JavaBeans approach). However, this can leave an object in an inconsistent state during it construction and of course precludes immutability since fields can’t be made
final
. - Use a Builder. This is the approach recommended by Bloch. The client creates a builder (often using a parameter-less constructor), then calls setter like methods for the values of interest (the rest assume default values), before finally calling a build method.
A few years back, I attended a talk in which Ted Young discussed taking the builder pattern a step further by using it for the construction of test objects, and it’s this approach that is discussed below. [Update: see Ted’s response to this post here]
Using the Builder pattern to construct test fixtures
Using a Builder allows test fixtures to be created more easily, and with clearer intent.
The type of test objects I typically use this Builder approach for are domain model objects, such as Account, User, Widget or whatever. I am a proponent of making such objects immutable.
For example:
public final class Account { private final Integer id; private final String name; private final AccountType type; private final BigDecimal balance; private final DateTime openDate; private final Status status; public Account(Integer id, String name, AccountType type, BigDecimal balance, DateTime openDate, Status status) { this.id = id; this.name = name; this.type = type; this.balance = balance; this.openDate = openDate; this.status = status; } public Integer getId() { return id; } //other getters, toString(), equals() and hashCode() omitted for brevity //no setters }
With such a class, you often run into the problem Bloch discussed. In this example, we have a single constructor that forces you to set all values, but we could also have many variations where some values can be omitted to use default values. So, creating an instance of such a class for tests can be somewhat painful, and more so if it has even more fields than this simple example. You are forced to provide values even for fields you may not care about for the test. That also makes it difficult to know which values are actually of interest for the test, and which are purely to make things compile.
A Builder can help.
Example
public class AccountBuilder { //account fields with default values Integer id = 1; String name = "default account name"; AccountType type = AccountType.CHECKING; BigDecimal balance = new BigDecimal(0); DateTime openDate = new DateTime(2013, 01, 01, 0, 0, 0); Status status = Status.ACTIVE; public AccountBuilder() {} public AccountBuilder withId(Integer id) { this.id = id; return this; } public AccountBuilder withName(String name) { this.name = name; return this; } public AccountBuilder withType(AccountType type) { this.type = type; return this; } public AccountBuilder withBalance(BigDecimal balance) { this.balance = balance; return this; } public AccountBuilder withOpenDate(DateTime openDate) { this.openDate = openDate; return this; } public AccountBuilder withStatus(Status status) { this.status = status; return this; } public Account build() { return new Account(id, name, type, balance, openDate, status); } }
Now you can create an Account object for testing much more easily.
Notes on using Builder for tests
- Default values
The default values used in a builder are a convenience to avoid exceptions. If your test needs specific values for a test, it is best to explicitly set them, rather than rely on any defaults. It makes the intent of your test clearer, plus minimizes the risk of inadvertently breaking tests if you ever need to change defaults (e.g. due to changing business requirements).
- Non-final fields
While the domain model class itself is immutable and hence has final fields. All fields in the Builder are, by design, non-final. Hence Builders are not thread-safe. So don’t reuse Builders; instead create a new instance for each test.
- Method order should not be significant
For the most part, the order that methods are called on a builder should not be significant, and the object will not be constructed until build() is called. This makes the builder easier to use and avoids unexpected surprises.
There are obvious and acceptable exceptions to this rule of thumb.
For example calling
Account account = new AccountBuilder() .withType(AccountType.SAVING) .withType(AccountType.CHECKING) .build();
is silly but allowed. It would just leave you with an account of type checking.
This is fine, but try to avoid more subtle causes of confusion, for example if you have a collection that can have something added, or the whole collection replaced (hence wiping out previous additions).
Advantages of using a Builder for tests
- Easy to read
The following declaration is not particularly clear:
Account account = new Account(1, "test", 10, ...);
This declaration is much clearer:
Account account = new AccountBuilder() .withId(1) .withName("test") .withBalance(10) .build();
As Bloch puts it, “The Builder pattern simulates named optional parameters”.
- Only specify values that are actually relevant to your test
If your test is only concerned with account balance and status:
Account account = new AccountBuilder() .withBalance(new BigDecimal(-100)) .withStatus(Status.OVERDRAWN) .build();
As opposed to having to specify every value in the Accounts constructor.
- Ability to create invalid objects
The constructor of the domain model class will likely (hopefully!) force you to create valid objects. In your tests, you may want to deliberately create invalid objects for testing.
Further enhancements
Convenience methods
You can add convenience methods for common scenarios used in testing.
For example
public AccountBuilder withNegativeBalance() { this.balance = new BigDecimal(-100); return this; }
Fixtures class
In addition to using a Builder class, I have also found it useful to have an associated fixtures class that provides pre-constructed instances for tests. These can make use of the Builder object for the construction (although there is nothing to stop you using the raw constructors too).
For example
public class AccountFixtures { //a shortcut to creating a basic Account object public final Account ACCOUNT = new AccountBuilder().build(); public final Account OVERDRAWN_CHECKING_ACCOUNT = new AccountBuilder() .withType(AccountType.CHECKING) .withNegativeBalance() .build(); public final Account CLOSED_SAVING_ACCOUNT = new AccountBuilder() .withType(AccountType.SAVING) .withZeroBalance() .withStatus(Status.CLOSED) .build(); }
I’ve seen this style before, and I think I need to use this more in my code.
Hi this is a really great simplified example, simplification being the AccountBuilder ( Concrete Builder ) is being used directly by the client .
Can we illustrate some thing with an interface for building (builder Account Builder interface) and then Concrete Account Builder. Basically , I am trying to decouple the Client from Concrete Builder.
BTW pardon me if while making this suggestion , I missed some thing fundamental to the Builder Design Pattern. I am starting to explore it .
Found What I Needed! Thank you.
Great post! thank you.
I like having the default values. Lets say you wanted to use the builder in your production code ~ would you / do you have a separate builder (without defaults) i.e. avoid the risk of test/default data getting mixed up with prod.