3 easy ways to write cleaner unit tests

Steve Merritt
3 min readMar 13, 2019

--

Photo by Clément H on Unsplash

Look, writing good unit tests is hard. We all know it. Everyone runs into it at some point. But that doesn’t mean that your mediocre tests need to be unreadable. Here are 3 tips that can make all the difference in keeping those average-ish tests simple, clean and sharp.

1. One test, one function.

Don’t lump multiple tests together in one function like this.

public void testBasicStuff() {
Bar bar = makeBar();
Bar expectedBar = makeBar().setBuzzer(3);
runBuzzProgram(bar);
assertThat(bar).isEqualTo(expectedBar);
Bar expectedBar2 = makeBar().setBuzzer(6);
runBuzzProgram(bar);
assertThat(bar).isEqualTo(expectedBar2);
Bar expectedBarFinal = makeBar().setBuzzer(0);
runBuzzShutdownProgram(bar);
assertThat(bar).isEqualTo(expectedBarFinal);
}

Lumping these 3 tests together is unfortunate because:

  • It makes it ambiguous whether stuff from the first test affects stuff from the second and third tests.
  • The starting state in the second and third tests is not explicit, since it’s “whatever the state is after the first test finishes”. This is much harder to reason about.

To fix this, explicitly set up the starting state for tests #2 and #3 in new functions:

public void testOne() {
Bar bar = makeBar();
Bar expectedBar = makeBar().setBuzzer(3);
runBuzzProgram(bar);
assertThat(bar).isEqualTo(expectedBar);
}
public void testTwo() {
// Here, we set up the same state that test #1 ended in.
Bar bar = makeBar().setValue(3);
Bar expectedBar2 = makeBar().setBuzzer(6);
runBuzzProgram(bar);
assertThat(bar).isEqualTo(expectedBar2);
}
public void testThree() {
// Here, we set up the same state that test #2 ended in.
Bar bar = makeBar().setValue(6);
Bar expectedBarFinal = makeBar().setBuzzer(0);
runBuzzShutdownProgram(bar);
assertThat(bar).isEqualTo(expectedBarFinal);
}

Notice how making the starting state explicit makes each test simpler to reason about, and to read any one of these tests only requires us to look at 4 lines of code. Wouldn’t you rather read this version?

2. Good test names.

public void testBasicFunctionality() {

Such a shame to see test names like that one, when they could have looked like this:

public void testMyFunc_updatesBar_inExpectedGeneralCase() {
...
}
public void testMyFunc_updatesBar_whenFooServiceReturnsError() {
...
}

Here, I don’t even need to read the test implementation to know what we’re testing, because we gave the test a useful name.

I personally like to structure my test names in the following way:

public void testSYSTEM_BEHAVIOR_CONDITION() {

For example:
public void testAuthenticate_rejectsUser_whenBadCredentialsProvided() {

3. AAA: Arrange, Act, Assert

Arrange: Set up the test. Mock outgoing RPCs, populate fake databases, etc.
Act: Run the function under test.
Assert: Check that the expected behavior occurred.

public void testMyFunc_updatesBar() {
Bar bar;
Bar expectedBar = makeBar().setBuzzer(3);
myFunc(bar);
assertThat(bar).isEqualTo(expectedBar);
}

This code, despite being 4 lines, is already a bit confusing because AAA is not separated. Why is expectedBar declared at the top? Is it going to be used in myFunc()? When does the setup finish, and when does the test actually get run?

You can prevent all of this confusion by separating these 3 steps in your code:

public void testMyFunc_updatesBar() {
Bar bar;
myFunc(bar); Bar expectedBar = makeBar().setBuzzer(3);
assertThat(bar).isEqualTo(expectedBar);
}

Here’s another example, this time in Python:

We haven’t seen what foo() does, or what supposeGarbleServiceReturns() does, or even makeSimpleUserWithExistingBaz(), but you can immediately figure out what is going on here because the test is named well, the 3 steps are separated, and it is commented appropriately.

None of these 3 strategies require any kind of major refactoring or debugging to implement, even in existing code. And yet, their impact on the readability and maintainability of large test suites can be massive. Please, for the sake of your coworkers and those who will someday inherit your codebase, format your unit tests!

📝 Read this story later in Journal.

🗞 Wake up every Sunday morning to the week’s most noteworthy Tech stories, opinions, and news waiting in your inbox: Get the noteworthy newsletter >

--

--

Steve Merritt

Engineering leader at Tenet. Built some parts of Google, Stadia, YouTube, and Facebook.