Logo Bryan Wong

Great Unit Testing

Bryan Bryan
October 15, 2024
4 min read
53 views
Table of Contents
index

Unit tests are the first line of defense against bugs costing businesses money, users and reputation.

They sit at the foundation of the test pyramid as the most common, granular and quickest to run automated tests.

Not writing them well opens you up to an increasing risk of errors and liability as your codebase grows in complexity. Avoiding them upfront often becomes a burden to compensate for at a later date especially when the original context is lost.

Great unit tests give you peace-of-mind about what you’ve implemented, insight into whether you’ve structured your units of code well, and unlock the ability to make future refactors or code migrations with confidence.

They should not be difficult to write.

These are some practical tips I wanted to share to help junior engineers level up faster.

1. Frame tests with the given-when-then structure

This is a convention that I personally like when structuring my unit tests.

Each part describes a linear journey which is easy to follow for yourself and the reader.

  • Given represents preconditions (i.e. things to set up)
  • When represents actions taken (i.e. the operation to test being executed)
  • Then represents expected outcomes (i.e. the result to check)

This structure is shown in the following example.

@Test
public void givenSufficientBalance_whenWithdraw_thenReturnSuccess() {
// given
// ...
// when
// ...
// then
// ...
}

2. Focus on state coverage over code coverage

Code coverage measures how many lines of code were executed by your tests. This is often reported as a metric and is useful as a general guide during code reviews.

However, it’s best to frame your thinking in terms of state coverage instead.

This measures how much you’ve tested your system’s behaviour across different inputs and conditions. Thus, forcing you to think more precisely about different ways your system might break.

3. Test state transitions

Test operations which depend on each other together, as well as different sequences of them which result in different outcomes.

Let’s take the example of a class which has separate methods to open a file, do some processing and close the file. Other than the obvious open-process-close sequence, we should also consider what happens if:

  1. You try to process or close a file before opening it
  2. You try to process a file >1 times after it has already been opened

4. Add a test every time you fix a bug

This gives you confidence that the bug won’t happen again.

5. Treat them like documentation

Tests are vital because they document your code’s expected behaviour to yourself and others into the future. Trust me, you’ll thank yourself later.

6. Keep them tidy

Test files are just as prone to code rot as any other code.

Here are some actions to consider:

  1. Parameterize your tests where applicable. Each unit test should cover one specific scenario, and parameterization allows you to execute the same unit test with different parameters without duplicating it.
  2. Group related tests together. It makes it easier to scan the file.
  3. Structure your code consistently. For example, always putting helper functions at the bottom of the test file.
  4. Use shared test fixtures or helper functions to reduce duplication.