Tobias Davis

Designing better software systems.

Articles | Contact | Newsletter | RSS

Site logo image

Don't DRY your tests?

There are two primary goals in a good test: documentation and assertion. When humans read code, good tests will show how that code was intended to be used–that’s a type of documentation. But tests are also code and work as an assertion of behavior–because comments are not executed, they often end up being inaccurate and misleading.

The assertion part of tests is pretty well known, so I won’t dwell on it very long here. You want to assert that certain inputs yield certain outputs predictably:

const transform = require('./transform.js')
assert(transform('aBcD') === 'ABCD', 'capitalizes all letters')

What’s not talked about as much is the documentation side of tests.

Documentation #

If you are a good programmer, you probably refactor your code to re-use functions and variables, and if you do that you might end up doing the same thing to your tests.

For example, suppose you had a function that looked like this:

function fetchSomeThings(params) {
if (params && params.filter === 'published') {
return fetchData('published', true)
} else {
return fetchData('published', false)
}
}

There are many things that might make that function better, but one good programming practice would lead you to take the static string variable and pull it out:

const FILTER = 'published'
function fetchSomeThings(params) {
if (params && params.filter === FILTER) {
return fetchData(FILTER, true)
} else {
return fetchData(FILTER, false)
}
}

When it comes to writing tests, you might think that you should do the same thing there as well. For example, look at the following test:

const test = require('tape')
const transform = require('./transform.js')
test('account id is normalized', t => {
t.equal(transform('abcd'), 'ABCD', 'lower is upper')
t.equal(transform('aBcD'), 'ABCD', 'mixed is upper')
t.equal(transform('ABCD'), 'ABCD', 'upper is upper')
})

In a similar way, a reasonable programmer would see that one of those properties is repeated, and would be tempted to pull that out into a constant near the top of the file:

const test = require('tape')
const transform = require('./transform.js')
const expected = 'ABCD'
test('account id is normalized', t => {
t.equal(transform('abcd'), expected, 'lower is upper')
t.equal(transform('aBcD'), expected, 'mixed is upper')
t.equal(transform('ABCD'), expected, 'upper is upper')
})

However, remember that one of the big goals of testing is documentation, so we need to ask the question: is the second example easier to read? In this small example both ways are about as easy to read, because the compared values are near each other, but I assert that the test that has the values near the comparison or assertion is almost always easier to read.

Imagine a longer test file that looks like this:

const expectedAccountId = 'account123'
// ...
// 100 lines later
// ...
t.equal(response.accountId, expectedAccountId, 'ids match')

In order to know what the expected value is, you would need to keep that expectedAccountId value in mind while reading the test document.

A human reading the test code needs to make sure the test is sensible and is asserting the right things. However, the human brain is not very good at keeping track of details, and so is more likely to miss errors in the test, which then turn into errors in the code.

There are many reasons why writing in a DRY (Don’t Repeat Yourself) mode is valuable, but be cautious when DRYing your tests. Focus instead on making them as easy as possible for humans to read.

Want to know if well designed tests can help you? Send me a message, I’d love to help make your project a success!


Your thoughts and feedback are always welcome! 🙇‍♂️