DEV Community

Cover image for Replace your test fixtures with builders
Parth Narielwala for Everly Health

Posted on

Replace your test fixtures with builders

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
}
Enter fullscreen mode Exit fullscreen mode

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,
})
Enter fullscreen mode Exit fullscreen mode

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')
})
Enter fullscreen mode Exit fullscreen mode

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',
})
Enter fullscreen mode Exit fullscreen mode

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` 🙌🏽
Enter fullscreen mode Exit fullscreen mode

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)