Shaun Abram
Technology and Leadership Blog
Code Camp: Building Better Tests in Java
Building Better Tests in Java
Speaker: Ted Young
Using the Builder pattern to make tests easier. The Builder Pattern separates the construction of a (complex) object from its representation. A pizza analogy was used to explain the concept.
A Pizza director says a pizza is built using a base, sauce, toppings
Possible Builders could then be
- ThinCrustBuilder, DeepPanBuilder
- HotSauceBulder, TomatoSuaceBuidler
- VegToppingsBuilder, MeatToppingsBuilder
Using a factory method can result in too many parameters.
So, you can create a factory method that defaults many values to solve this – better but…
You may want to control certain values at certain times and things then start getting complicated (too many methods/combinations).
This is basically the Object Mother pattern.
Can cause Merge hell, Copy-n-paste, too long (all sounds familiar!)
Solution…
Builders and chained Methods…
Lots of defaults (if you don’t specify, use default value, e.g. random numbers, default values…)
e.g.
Account account = new AccountBuilder()
.withDefaultBillingPlan()
.withPolicies(new PolicyBuilder.withDefaultPolicyPeriod())
.create();
where for example, withDefaultBillingPlan() returns a Builder rather than an object such as BillingPlan, Account etc
Advantages to this approach:
- Specify data not possible through the UI (although isn’t this just an advantage of test cases in general, rather than a Builder pattern specifically?)
- Data is always consistent (but not necessarily valid – you want to test ‘invalid’ scenarios/objects also!)
- Fills in unspecified data with reasonable defaults (reasonable = wont throw exception)
- Extensible
- Can export to SQL
Can migrate bit by bit from huge DataGen/DomainFactory class to Builders,
Or can do big bang.
Writing Builder methods….
- Write methods that always return ‘this’
- Write English-like methods
- creation does happen until create() is called
e.g.
public AccountBuilder withBillingPlan(BillingPlan bp) {
account.setBillingPlan(bp);
return this;
}
This is what allows for chaining.
e.g.
asSmallBusiness();
instead of
withSegment(AccountSegment.SMALLBUSINESS);
e.g.
policy = new PolicyBuilder()
.onAccount(account)
.withPlan()
.create();
should be the same as
policy = new PolicyBuilder()
.withPlan()
.onAccount(account)
.create();
Overall:
This was one of the most useful talks I attended this weekend. Perhaps mainly because the project I am currently working on has exactly the kind of monster test-generating class that Ted talked about. I would like to try implementing the Builder patter approach he advocates. I had a chance to discuss the benefits with Ted after the lecture and he summarized them as:
- Easy to use default values
- Break the test data generation in to multiple, easier to manage classes rather than one monster one
- The code is much easier to understand
e.g.Account account = new AccountBuilder() .withDefaultBillingPlan() .withStandardPeriod())
is much easier to understand than
Account account = new Account(1, 4);
Links:
Builder Pattern (Wikipedia)