Unit Testing: Short and Sweet
This is the short and sweet introduction to unit testing I always wanted to have as a reference for colleagues, friends, and family. Enjoy.
What Is a Unit Test?
A unit test ensures that an individual unit of code behaves as intended. A unit is a minimal chunk of code that can be tested in isolation, such as a function or a class.
A test is not a unit test if it talks to a database, requires network access, writes to the filesystem, or requires a complex environment to be setup. This is crucial to keep things independent, robust, and fast.
Unit tests form the basis of the test automation pyramid. Write lots of them. Make them run fast.
Why Unit Testing?
- Correctness: Unit testing helps you to write correct code. You detect errors early during development and avoid costly changes later on.
- Evolution: Unit tests allow you to evolve your code over time. You can refactor your code without fear of introducing subtle bugs. This is key for reducing technical debt and improving your design.
- Design: Unit testing encourages good design practices such as modularization and minimizing dependencies between components.
- Documentation: Unit tests document the behavior of the code. They serve as a form of executable specification.
Unit Testing Best Practices
Good unit tests follow FIRST principles:
- Fast: Run fast so that developers can run them frequently
- Independent: Run standalone, in any order, without dependencies
- Repeatable: Give consistent and reliable results on each run
- Self-validating: Indicate success or failure without manual inspection
- Timely: Written together with production code to foster testable design
Clean unit tests follow the AAA pattern:
- Arrange: Setup the test
- Act: Execute the function under test
- Assert: Use an appropriate assert
This pattern implies testing a single concept per test case, using a single assertion.
Start with testing the happy path: Make sure that intended usage of the code produces the desired result.
Don’t forget the unhappy paths: Gracefully handle corner cases, unexpected input, pre-condition violations, or error conditions.
Aim to cover your complete public API with unit tests. Trivial functions like getters and setters might be excluded.
Use code coverage tools to analyze which code paths are actually triggered by your tests.
Let your testing code follow the same high standards as your production code: well designed, thoughtfully partitioned, and using meaningful names. Comments explain what is tested and why.
Refactor common setup code into a fixture to avoid repetition.
If your tests use tolerances, make them configurable so that you can check that nothing changed in the results.
Next time you…
- add new code, write at least one unit test covering the happy path
- find a bug, write a unit test that would have caught it
- refactor a part of your code, bring it under test first
When time and circumstances permit: Setup code coverage and bring all critical parts of your code under test.
The Bottom Line
Unit testing is like exercising: you know it’s good for you, but it can be tough to get started. It’s even more difficult to stick to it after the initial enthusiasm wears off. It’s an investment in the future of your code that pays off.
- Test-Driven Development: By Example by Kent Beck
- Refactoring: Improving the Design of Existing Code by Martin Fowler
- Working Effectively with Legacy Code by Michael Feathers
- Testing in the Twenties by Tim Bray
- You Still Don’t Know How to Do Unit Testing by Erik Dietrich
Other articles I’ve written on testing: