I know most of us are not the biggest fans of writing unit tests for our code. We just want to write our awesome, bug free code and ship it fast! As ideal as this sounds, it’s not realistic and bugs will happen sooner or later - especially if other developers are working on your code without much context and breaks something.
We can all agree that unit testing can get tedious and take forever to write, especially when you have to manually defined your test data. Test data can come in all different shapes and sizes but many people are most familiar with test fixtures
.
Test fixtures are essentially example data objects that can be used in many instances of your unit tests. In Javascript, you’ll find them typically in a .json
file or a Javascript file as an object {}
type.
As your application grows, so will your test fixtures with different variations of the same object type. This can create many obscure one-off fixtures that may be difficult to modify and fit your specific test needs.
Object Mother
The Object Mother is one pattern that can help solve this problem. This pattern extracts these fixtures in a more factory-like pattern.
var testOrder = {
order_id: 123,
line_items: [{
line_item_id: 888,
product_uuid: 'aaa-bbb-111-222'
}],
total: '10.99',
}
/* becomes */
var createSmallOrder = () => ({
order_id: 123,
line_items: [{
line_item_id: 888,
product_uuid: 'aaa-bbb-111-222'
}],
total: '10.99',
})
var createLargeOrder = () => ({
order_id: 123,
line_items: [{
line_item_id: 888,
product_uuid: 'aaa-bbb-111-222'
},
/* redacted for brevity */
{
line_item_id: 101010,
product_uuid: 'aaa-bbb-111-222'
}],
total: '100.25',
})
var OrderMother = {
createSmallOrder,
createLargeOrder
}
This pattern is more useful in class-based programming languages that have unique getters and setters, where one could call a method on the Object Mother and only affect that instance but it doesn’t quite work for Javascript land.
Builders
This is where we could look towards “builders”. I like to use the word “builders” instead of factory, because factories remind me of having a getter, and more importantly in this case a setter, on the factory object to make any changes to the test data we need for the specific test case. Builders give us a fresh new object where we can override any property and works with immutable objects.
If we look at our previous example
var testOrder = {
order_id: 123,
line_items: [{
line_item_id: 888,
product_uuid: 'aaa-bbb-111-222'
}],
total: '10.99',
}
/* this would become */
const baseLineItem = {
line_item_id: 888,
product_uuid: 'aaa-bbb-111-222'
}
const baseOrder = {
order_id: 123,
line_items: [baseLineItem],
total: '10.99',
}
const aLineItem = (overrides) => ({
...baseLineItem,
...overrides,
})
const anOrder = (overrides) => ({
...baseLineItem,
...overrides,
})
This allows us to essentially build our test objects with defaults while being able to override what is important to the test. If the test if checking for the total price to be of a certain value, we can set that value manually and in our tests expect that value to appear
test('outputs proper total cost', () => {
const order = anOrder({ total: '19.99'})
const value = getOrderTotalFormatted(order)
expect(value).toBe('$19.99')
})
Here we don’t care about what the order_id
is or what the line_items
are, we just want to know that the output is depending on what we are passing in and defining explicitly.
Specialized objects
This is a very simple and broad example but we can even have some builders that act like Object Mothers, in that they are objects for a specific use case.
const baseLineItem = {
line_item_id: 888,
product_uuid: 'aaa-bbb-111-222'
}
const baseOrder = {
order_id: 123,
line_items: [baseLineItem],
total: '10.99',
}
const aLineItem = (overrides) => ({
...baseLineItem,
...overrides,
})
const anOrder = (overrides) => ({
...baseLineItem,
...overrides,
})
const anEmptyOrder = () => anOrder({
line_items: [],
total: '0.00',
})
Notice that we are using the simple builder here to define anEmptyOrder
because we only care about setting certain properties and properties like order_id
is irrelevant to this specialized case.
Resilient to change
Another powerful aspect of this is that if the order
object gains a new property, let’s say shipping_method_id
, then only the baseOrder
needs that new property and all the builders will now have it!
/* redacted for brevity */
const baseOrder = {
order_id: 123,
line_items: [baseLineItem],
total: '10.99',
shipping_method_id: 3
}
/* redacted for brevity */
const anEmptyOrder = () => anOrder({
line_items: [],
total: '0.00',
}) // now this will come with `shipping_method_id: 3` 🙌🏽
Conclusion
I’ve been using builders for the past several years and they have made writing unit tests less painful. I love the fact that I can use a default test object and override just what I need in a way it’s clear when reading the tests what the important details are. I encourage you to give this a shot and hopefully this make writing tests a bit less painful for you as well. #happycoding
Top comments (0)