Unit Testing Guides

llllzllll
6 min readNov 19, 2022

Unit testing is a broad topic, there’s been an abundance of articles talking about unit testing. I’ll use this article as a chance to share my knowledge writing unit tests, and the lessons I learned from my mistakes and other articles.

Objective of Unit Testing

The most important objective to write tests is to define the behaviors of the software and make sure they are consistent when new changes are introduced. We have tests at all different levels: E2E tests, integration tests, and unit tests. For unit tests, it’s about their corresponding software units.

Unit Test as Software Behavior Contract

In Refactoring by Martin Fowler[1], one of the most important thing before refactoring is to make sure that there is a safe net of unit tests present, so that any unexpected behavior change during a refactor will be captured by failing unit tests. However, the benefits of unit tests go way beyond refactoring, for every new change we add, we have the risk of breaking existing behaviors.

In JavaScript land, the test files end with .spec.js extension [2], indicating tests are software specification. I think it’s a very good description of the role unit test should play — they are specification for the expected behavior of a software.

Automation

At the early stage of the development cycle for a software, simply manual testing will cover all the need for testing. However, as the complexity grows, you’ll notice that manual testing will be a bottleneck to cover all the cases. If you still try covering all the cases with manual testing, you’ll either waste most of your time manual testing, or a lot of test cases will fall through the crack.

Eventually, you made the right choice to start cover your manual testing test cases with unit tests! If they are good unit tests, then you save yourself countless time from repeatedly manual testing:

You need to spend minutes, even hours to cover all the cases with manual testing, and making things even worse, as a human being, you tend to forget some edge cases while performing manual testing. However, with unit tests, the time is reduced to milliseconds. It becomes way more scalable doing tests. Developers can run tests frequently to check if the new change works as expected. Test Driven Development [3] is a good example of running test frequently and updating expected behavior frequently throughout the development phase.

That’s the power of good unit tests: they serve as automation, and liberate developers from tedious manual testings.

Principles of Good Unit Testing

Ok. The objectives mentioned above both assumed your write good unit tests, which is the hard part. I’ll list some criteria I believe could be the indication of good unit tests.

I put labels before each principle to indicate the aspect the principle is about, here are all the labels:

  • Methodology: about how to write good tests;
  • Effectiveness: the attributes that defines good tests;
  • Engineering efficiency: achieve more with less engineering efforts

1. [Methodology] Treat writing tests as proving theorems

If there’s only one advice I have to give, this will be it. This resonates with Unit Test as Software Behavior Contract mentioned above.

The general guideline is: we want to establish the causal effect among the given inputs, environments, and the expected causal outcome after certain event (e.g.: an API called).

An honorable mention about test double [4] here: if you introduce a test double, you also introduce an assumption. For example: if you mock a method call, and specify its return value(s), you are assuming that when the function is called, it will always return the value(s) as specified. Developers should really think twice whether the assumption makes sense before using test doubles.

Using too many test doubles is usually a not good sign, because it’s equivalent to making too many assumptions while proving a theorem. It’s especially so when your assumption (test double) does not make much sense. By using a mock, you lose the great chance to test how your software components interact with each others.

There’s another blog post from Accenture [10] giving more arguments about using test doubles cautiously from software engineering point of view.

Note: If you are not familiar with test doubles, it’s highly recommended to go over the referenced link in [4].

2. [Effectiveness] Good tests should have high precision and recall [9].

This basically means that:

  • the tests should fail when the expected behavior no longer matches the actual behavior. (True negative*, and no false positive)
  • The tests should not fail when there’s no behavior change. (True positive, and no false negative)

Both false negative and false positive from a test are indication of bad tests: False negative creates too much noise, that developers will lose their caution when seeing it fails, and eventually, may slip over some real bugs.

False positive will outright slip bugs into your production.

3. [Effectiveness] Good tests should run fast.

That means avoid external dependencies if possible. Use test doubles [4] like mocks or fakes to make sure the expensive behavior can be achieved with cheaper implementations.

4. [Engineering Efficiency] Good tests should be composable.

Tests, like general software, should be composable, so that writing new tests should not be a huge overhaul. Non-composable tests make the tests unmaintainable and also takes longer time to write, which eventually discourage people off from adding new unit tests to the system. That said, they should also conform the SOLID principles [5].

A test setup usually follows AAA (Arrange, Act, Assert) pattern [6]. One way to achieve the composability is to combine the AAA with SOLID principle, so each ‘A’ is well encapsulated, especially the ‘Arrange’ step, which has great potential for encapsulation with test data factory pattern.

Overall, having composable test setup is a great leverage to help write more tests with less engineering efforts.

5. [Engineering Efficiency] Sufficient test coverage.

Test coverage is usually referred to as the lines of code covered by tests. There has been a long debate about using test coverage as the objective of writing unit tests. Martin Fowler once wrote about TestCoverage [7] in a 2012 post, that it could be counter productive to use test coverage as the objective for writing tests.

If you aim for a magic number for test coverage, developers will eventually hack their way to achieve that number without contributing meaningful test cases.

There’s also a diminishing return as test coverage goes up. We should avoid letting writing tests become a blackhole for development time.

The main objective, however, should be:

- You rarely get bugs that escape into production, and

- You are rarely hesitant to change some code for fear it will cause production bugs.

That’s also why I said “sufficient test coverage” instead of high test coverage for this part.

6. [Methodology] Focus on how a function/API is used while writing test, instead of specific implementation details.

It’s important to follow the top down approach, so that the test ensures the API contract as mentioned in the Objective of Unit Testing — Unit Test as Software Behavior Contract section earlier. Kent C. Dodds wrote a good blog — Testing Implementation Details [8]. It’s an excellent post that I highly recommend reading. This echos with the principle: Good tests should have high precision and recall. And it’s a good guideline to ensure that.

Conclusion

Those are my general criteria for writing good unit tests. All the links in the reference section below are good reads about writing good tests. I highly recommend reading through most of them. For some quick inspiring reads, I recommend reading the blog posts [4][6][7][8][10] first to learn about their useful opinions in writing tests. For the more advanced topic like writing composable tests, I also recommend reading all the 5 principles in the SOLID principles [5].

[*]: For clarify, I say negative for test failures, and positive for test successes.

References

[1] Refactoring: Improving the Design of Existing Code

[2] StackOverflow: What does “spec” mean in Javascript Testing

[3] Wikipedia — Test Driven Development

[4] Martin Fowler — Test Doubles

[5] Wikipedia — SOLID principles

[6] Unit Testing and the Arrange, Act and Assert (AAA) Pattern

[7] Martin Fowler — TestCoverage

[8] Kent C. Dodds — Testing Implementation Details

[9] Wikipedia — Precision/ Recall

[10] Accenture — To mock or not to mock: is that even a question?

--

--