- Published on
Using a builder pattern for tests
- Authors
- Name
- Rouan Wilsenach
- @rouanw
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.