Code review: organize your tests for your sanity

I really didn’t want to write this, but every good link about structuring unit tests is dead. And I don’t want to hype this too much, but learning to arrange my tests in a certain order was one of the biggest productivity gains in my career.

This is a "Code review" post. This is designed to be copy pasted into code reviews

You should arrange your test into three distinct sections, AAA: Arrange, Act, and Assert. You’ll also see articles touting four sections called SEVT: Setup, Execute, Verify, and Teardown, but in practice, we never put teardown in the actual tests because then they would only run if the test succeeds.

  • Arrange – Set up the conditions for the test
  • Act – Execute the code
  • Assert – Verify your assumptions

If you arrange all your tests like this, then every test will looks familiar even if you’ve never seen before, which mean you’ll be able to understand unfamiliar code much faster.

1. Make the sections obvious

The easiest way to convey the three sections is to group the lines into the three sections separated by a blank line. If your test only has two or three lines, you probably don’t need the blank lines, and if for some reason your test is so long that it can’t fit on one screen, you might need comments to label the sections.

Bad:

expected = {...}
user = {}

stubs...

result = foo(user)
assert result == expected

Better:

user = {}
stubs...

result = foo(user)

expected = {...}
assert result == expected

2. Your first test

I like to make my first test case either a trivial case (for pure functions) or the happy path (for request/response). And I try to get these tests to have zero lines of setup.

Bad:

test("knight is worthy")
  user =
    id: 1234
    email: lancelot@camelot.silly.place
    name: Sir Lancelot of Camelot
    favorite_color: yellow  # This should be blue
    quest: to seek the Holy Grail
    ...a million other things

  assert user.is_worthy()

Good:

setUp()
  user =
    id: 1234
    email: lancelot@camelot.silly.place
    name: Sir Lancelot of Camelot
    favorite_color: yellow  # This should be blue
    quest: to seek the Holy Grail
    ...a million other things

test("knight is worthy")
  assert user.is_worthy()

2. Copy paste your tests

New test scenarios should be a variation of an existing test; every test starts as a copy paste. This repetition makes your code easy for to read because everything looks familiar! Let’s say we need to check if the user is no longer worthy now.

One consequence is you’ll often end up with extra assertions. I usually keep them, but it’s a balance. If you’re not sure, I ask myself: “is this assertion relevant to the test name?”

3. Make the “arrange” section as small as possible

Minimize the setup for all your tests. The higher up you can get the “act” section of your test, the more readable your tests will be. Remember that the “arrange”/setup section is boilerplate, and we hate boilerplate.

A User object may require twelve things, but your arrange section should have none of them. Take advantage of fixtures, before blocks, factories; whatever tools you have at your disposal.

Bad:

user =
  id: 1234
  email: lancelot@camelot.silly.place
  name: Sir Lancelot of Camelot
  favorite_color: yellow
  quest: to seek the Holy Grail
  ...a million other things

assert user.is_worthy() == false

Good:

user.favorite_color = blue

assert user.is_worthy() == false

In the “bad” example, it’s not clear why this user fails the worthiness test. The reader is expected to compare it to a good example or to know the data. On top of that, it’s many lines before we actually get past the boilerplate. In the “good” example, it’s clear that the user has the wrong color.

Places this doesn’t work

You’ll be able to split up tests into these sections almost every time, but there are some common exceptions:

  1. Events, when you need to set up assertions .on('end', () => { assert ... } as part of the test setup
  2. Catching exceptions, you’ll be able to arrange in AAA order, but you won’t be able to add line breaks.


Better articles about this subject:

Leave a Reply

Your email address will not be published. Required fields are marked *