Writing Test Data Builders: Make It Easy on Yourself
Make It Easy is a tiny framework to simplify writing Test Data Builders in Java, cutting down on duplicate and boilerplate code. There is also a .NET port by Graeme Foster.
I've used Test Data Builders on a few projects now and found that they really pay off when the team share a common system of names for syntactic sugar, such as static factory functions for creating new builders. I've often written a new test as I'd like to read it and found that the IDE prompts me to import all the syntactic sugar I need: other team members have already written it with exactly the same names as I would have used. This really speeds up the writing of tests and helps everyone write tests that are readable and flexible.
However, I've never been satisfied with the way of implementing Test Data Builders that I described previously on this site and in our book. There is far too much repetitive boilerplate code and duplication. All that code has to be written and then maintained as the system's domain classes evolve. Overall, I think Test Data Builders have a positive effect, but a lot can be done to reduce the overheads of using them.
For example, consider writing builders for the following class hierarchy:
public abstract class Fruit { private double ripeness = 0.0; public void ripen(double amount) { ripeness = Math.min(1.0, ripeness+amount); } ... } public class Apple extends Fruit { private int leaves; public Apple(int leaves) { this.leaves = leaves; } ... } public class Banana extends Fruit { public final double curve; public Banana(double curve) { this.curve = curve; } ... }
To write a builder for, say, Bananas, I have to: declare the properties of a Banana; specify the default values of those properties; write a method to build a Banana with the property values collected by the builder; define a static factory function to act as syntactic sugar; make the constructor private to enforce use of the factory function; write chained "with..." methods that capture properties explicitly defined by tests; and write a "but" method so that tests can define builders relative to the properties of other builders. That requires a lot of code. For the Banana class, which has just two properties, it involves all of this:
public class BananaBuilder implements Builder<Banana> { private double ripeness = 0.0; private double curve = 0.5; private BananaBuilder() {} public static BananaBuilder aBanana() { return new BananaBuilder(); } public Banana build() { Banana banana = new Banana(curve); banana.ripen(ripeness); return banana; } public BananaBuilder withRipeness(double ripeness){ this.ripeness = ripeness; return this; } public BananaBuilder withCurve(double curve) { this.curve = curve; return this; } public BananaBuilder but() { return new BananaBuilder() .withRipeness(ripeness) .withCurve(curve); } }
And I'd have to do the same for Apples.
Really, the only thing that I care about a builder of Bananas is: the properties of Banana objects; the default values used for each property when a test doesn't explicitly supply a value; how a Banana is constructed with those property values. The rest is boilerplate code.
Make It Easy is a framework that lets me create Test Data Builders by defining only those details. Make It Easy takes care of the boilerplate: gathering property values and defining the syntactic sugar that makes tests easy to read.
For example, to define a builder for Bananas, I only have to specify the following (and the ripeness property can be used for Apple builders as well).
public static final Property<Fruit,Double> ripeness = newProperty(); public static final Property<Banana,Double> curve = newProperty(); public static final Instantiator<Banana> Banana = new Instantiator<Banana>() { @Override public Banana instantiate(PropertyLookup<Banana> lookup) { Banana banana = new Banana(lookup.valueOf(curve, 0.5)); banana.ripen(lookup.valueOf(ripeness, 0.0)); return banana; } };
The code to create Bananas with Make It Easy looks like this:
Banana defaultBanana = make(a(Banana)); Banana squishyBanana = make(a(Banana, with(ripeness, 1.0))); Banana straightBanana = make(a(Banana, with(curve, 0.0)));
To create a builder (or "maker" in the framework's lingo) that can be used multiple times:
Maker<Banana> anIllegallyCurvedBananaWithinTheEU = a(Banana, with(curve, 45.0)); Banana naughtyBanana = make(anIllegallyCurvedBananaWithinTheEU);
To define makers in terms of other makers:
Maker<Banana> aBananaThatCanBeUsedInTheManufactureOfSmoothies = anIllegallyCurvedBananaWithinTheEU.but(with(ripeness,1.0));
There is also syntactic sugar for making lists and sets of objects. See the example code for more details.
Make It Easy 1.1.1 is available for download now.
Update 02/02/2010: Graeme Foster has ported Make It Easy to C#.