🐳
Published on

Using a builder pattern for tests

Authors

What is a builder for test data?

It's a way to create test data with sensible defaults. Something like:

const buildPerson = (overrides) => {
  const defaultPerson = {
    id: guid(),
    firstName: 'Ada',
    lastName: 'Lovelace',
    address: '1 Computer Avenue',
    phoneNumber: '123',
  }
  return { ...defaultPerson, ...overrides }
}

The above is a simplistic example. You'll almost certainly have nested data structures at some point, at which point it might look more like:

const buildAddress = (overrides) => {
  const defaultAddress = {
    id: guid(),
    line1: '1 Computer Avenue',
    line2: 'Somewhere',
    postCode: 'abc123',
  }
  return { ...defaultAddress, ...overrides }
}

const buildPerson = (overrides) => {
  const defaultPerson = {
    id: guid(),
    firstName: 'Ada',
    lastName: 'Lovelace',
    address: buildAddress({ postCode: 'bad data' }),
    phoneNumber: '123',
  }
  return { ...defaultPerson, ...overrides }
}

Why have a builder for test data?

The main reason I prefer the builder pattern is because it helps the readability (and therefore maintainability) of the tests.

With:

it('should capitalise first name', () => {
  const person = buildPerson({ name: 'sue' })
  // do things
  expect(result.person.name).to.equal('Sue')
})

It’s much easier to see what setup data is important to the test. You can ignore the rest.

With:

it('should capitalise first name', () => {
  const person = require('../data/person')
  // do things
  expect(result.person.name).to.equal('Sue')
})

It’s hard to see what data is important, so tests are much harder to read.

The builder avoids you having to have:

it('should capitalise first name', () => {
  const person = {
    name: 'sue',
    lots: 'of',
    other: 'things',
    you: 'just',
    do: 'not',
    care: 'about',
  }
  // do things
  expect(result.person.name).to.equal('Sue')
})

Of course you could do:

it('should capitalise first name', () => {
  const person = { ...require('../data/person'), name: 'sue' }
  // do things
  expect(result.person.name).to.equal('Sue')
})

But

  • it’s not as neat
  • It increases unintended coupling between tests e.g. your person file will start including address because the address tests need it even though you don’t care about it and may start to include weird edge cases that aren’t relevant. The file gets big and you keep having to refer back to it and sift through info you don’t care about. The definition of what the ā€˜minimum’ version of the data is becomes murky.
  • You can’t absorb any complexity from your tests. E.g. maybe when you create a person they automatically get a guid , or maybe if you instantiate them with a birthday older than X they automatically get a canDrink: true field.
  • I’m personally a big fan of giving tests their own data. I don’t like it when all the tests use the same userId , same name, same dates, because you end up with less diverse test data and therefore you’re testing fewer scenarios.