Categories
Readable JavaScript Tests with Object Builders
Last published on August 03, 2015 by Marcos Iglesias
This article was originally posted on Eventbrite's Engineering Blog.
Building objects purely for testing purposes is often tedious. Let's walk through the wondrous world of using design patterns to improve your JavaScript tests.
Over the past weeks we've been porting features to our new Event pages. Events are complicated, and the front end of the event page that supports them is feature-rich and has to deal with all sorts of different states an event can be in, in addition to all the types of ticket an event can have. As you can imagine, writing tests to cater for all these different scenarios can get tricky fast.
We have been pretty rigorous with test driven development in this project, including refactoring our tests. We have tried different strategies to build the test objects and started with something like this:
//
// Broad categories
//
var orders = {
free: {
// Some attributes
},
paid: {
// Some attributes
},
};
This quick fix worked at the beginning, but we recognized it wasn’t scalable. We had to support a large amount of different variations, so this simple solution involved a lot of duplication. After noticing this issue, it was transformed into this code:
//
// Extending categories and specific cases
//
var defaultOrderOptions = {
free: {
// Some attributes
},
paid: {
// Some attributes
},
},
specificOrderOptions = {
waitingList: {
// Some specific attributes
},
soldOut: {
// Some specific attributes
},
};
function getOrderOptions(category, type) {
if (isValidOptions(category, type)) {
return _.extend(
{},
defaultOrderOptions[category],
specificOrderOptions[type]
);
} else {
throw new Error("getOrderOptions error!");
}
}
Here we are using a method to extend the default attributes with the more specific ones. However, it was clear we were duplicating code on different files. We had to extract a helper module to share this object creation function between Entities, Views and Components. It was a bit closer to a final solution and it looked more or less like this:
//
// Encapsulated on a module to share between files
//
var defaultOrderOptions = {
//order model categories
},
specificOrderOptions = {
//order model specific types
};
function getOrderOptions(category, type) {
if ( isValidOptions(category, type) ) {
return _.extend({}, defaultOrderOptions[category], specificOrderOptions[type]);
} else {
throw new Error('getOrderOptions error!');
}
}
function getTicketOptions(category, type) { ... }
return {
getTicketOptions: getTicketOptions,
getOrderOptions: getOrderOptions
};
The object creation was still being performed on the beforeEach functions of our test suites. There was also some duplication, a bit less encapsulation and it was tedious when reading one after another.
The breakthrough for us happened when reading Growing Object-Oriented Software, Guided by Tests, and in Chapter 22: Constructing Complex Test Data we learned about a couple of design patterns useful to improve this code.
The Object Mother
This technique is based in a class that provides a bunch of factory methods that create the objects for the tests. It is a simple API it’s called like this:
//
// Using an Object Mother
//
this.orderModel = ExampleOrderModels.orderWithFreeTickets();
this.orderModel2 =
ExampleOrderModels.orderWithOneFreeTicketAndOneSoldOutTicket();
this.orderModel3 = ExampleOrderModels.orderWithOnePaidTicketNearSalesEnd();
It's pretty readable, encapsulates the logic and gives it an structure, and it can be reused. This approach could cut it if you just need to create a bunch of different objects, but if you need to support a lot of variance it will grow like faster than a Saint Bernard puppy, making it awfully tough to maintain.
Test Data Builders
The best solution in our case was using the Builder Pattern to create our test objects. It consists of the following, pulled from the book:
"For a class that requires complex setup, we create a test data builder that has a field for each constructor parameter, initialized to a safe value. The builder has “chainable” public methods for overwriting the values in its fields and, by convention, a build() method that is called last to create a new instance of the target object from the field values"
In Javascript, we can get a default instance calling it like this within our beforeEach functions:
//
// Default builder in action
//
this.orderModel = new OrderBuilder().build();
The variations follow a similar syntax:
//
// Using a variation
//
this.orderModel = new OrderBuilder()
.with3PaidTickets()
.withTicketsRemaining()
.build();
looks great, doesn't it? And the best part is that you can compose them together with other builders:
//
// Composing with other builders
//
this.orderModel = new OrderBuilder()
.fromCustomer(new CustomerBuilder().build())
.with3PaidTickets()
.withTicketsRemaining()
.build();
or create base builders and then apply the differences, building them at the very end:
//
// Creating a base case
//
var regularPaidOrder = new OrderBuilder().with2PaidTickets();
this.orderModelRemaining = regularPaidOrder.withTicketsRemaining().build();
this.orderModelPromo = regularPaidOrder.withPromoCode().build();
Here is a gist of our implementation of the test object builder. Please feel free to leave your comments on the gist, or keep the conversation alive by tweeting me @golodhros.
Builder Advantages
The first advantage of this builder pattern is its encapsulation, as the object creation process is wrapped on the builder. It decouples the objects and their parameters from the code at test, so if any of the input parameters changes, we just need to modify one file.
It’s also really convenient, as the default case is super simple./p>
However, there is something that we have enjoyed the most from this pattern, and that’s the readability improvements. It allows a clean, declarative and intuitive code, where its easy to find the differences between the objects used for each test.
Wrapping Up
Test builders are great if you need to build exhaustive tests for a feature or module. They could be also overkill if you need to test just a couple of variants. Even in that case, we would use them, as their improved readability has made it become one of our favorite patterns.
photo credit: HMS Ark Royal - 10th March 1981 (license)