Tobias Davis

Designing better software systems.

Articles | Contact | Newsletter | RSS

Site logo image

Property names and values in tests

Recently I was reviewing some code from a coworker and saw a test that looked like this:

const results = importantWork('userId', 'resourceId')
assert.equals(results, expected)

And that importantWork function had a signature that looked like this:

importantWork(userId: string, resourceId: string): ImportantResults

In the test, the string passed in matches the parameter name of the function, e.g. userId and resourceId are both the property name and the value used in the test.

Imperfect compilers #

When humans read code, we rely on inference of symbols to interpret what we’re reading.

Our brains might seem like code compilers, but instead of following strict programmatic rules we actually rely on a neural fuzzing approach where the different symbols load into the hippocampus and cycle through the cortex, building a best-fit model through neuron activation.

Any time the symbols we read seem to infer something unrelated, that inference loads in and through the loop whether you like it or not (see Daniel Kahneman’s work on this topic), meaning we waste brain cycles on things that look related to other things but aren’t.

Alternate but bad approaches #

If you use MongoDB auto-generated IDs and you try to write your test data like real data, you might end up trying something like this:

const results = importantWork('507f1f77bcf86cd799439011', '1c77bcf3207f4390116cd799')
assert.equals(results, expected)

But this also causes wasted brain cycles due to automatic inference: the brain is spending loops digging up anything it can recall about the IDs 507f... and 1c77... but within these tests those exact numbers aren’t important.

It also obfuscates what’s expected versus a bug in code: did the person writing the test realize that the IDs were the right ones? In the right order?

It also increases mental load if you need to keep track of different identifiers through a test. You can’t actually memorize the entire identifier string, so you rely on memorizing some amount of characters, but this often turns into scrolling back and forth to keep track of the different tokens.

The lack of clarity might suggest putting the IDs into variables, e.g.:

const testUserId = '507f1f77bcf86cd799439011'
const testResourceId = '1c77bcf3207f4390116cd799'
// ... later ...
const results = importantWork(testUserId, testResourceId)
assert.equals(results, expected)

But, as I’ve written before, there’s significant mental overhead to writing DRY tests so you shouldn’t do that.

What to Do Differently #

Instead, test data should clearly be test data, but human readable.

One way that seems to work well in projects I’ve worked on is a type prefix followed by a number, e.g. something like this:

const results = importantWork('user-1', 'resource-1')
assert.equals(results, expected)

This infers two things to the brain:

  1. There is an intended type, e.g. user or resource.
  2. This is the nth of these types.

If you have more than one of a type in a test, simply increment the number, e.g. user-1, user-2, etc.

Note: I’ve also noted what seems to be shorter inference loading by always padding the number to one more than the max, e.g. if you have only 1 user you would do user-01 but if you had 10 in a test, you would use user-001 through user-010. The leading zero seems to cue the brain to the identifier more easily, but this could be a pattern familiarity bias on my part.

If you have multiple unrelated tests in one file, you can reuse these identifiers. For example:

describe('how users are loaded', () => {
test('loading with related resources', async () => {
const results = fetchUser('user-01', 'resource-01')
assert.equals(results, expected)
})
test('loading without related resources', async () => {
const results = fetchUser('user-01')
assert.equals(results, expected)
})
})

So long as the different scopes are clearly delineated, for example different test blocks, reuse of symbols doesn’t seem to cause mental inference confusion, and is even a good idea if you want to infer similarity.

For example, seeing that the user identifiers above were both user-01 made it easier to see that they were identical except for the second function parameter. If the two tests used different user identifiers, such as user-01 and user-02, there would be wasted brain loops trying to infer meaning to the difference.

Further reading #

Empirical evidence on code quality, complexity, and readability seems to be inconclusive at best, but probably misleading.

Know of any research on this topic? Send me details, I’d love a deeper dive!


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