Here are some notes on the course “Building a Pragmatic Unit Test Suite” by Vladimir Khorikov.
Goals and Guidelines
Unit tests help with confidence: you know that changes don’t break functionality.
Not all unit tests are equal.
Coverage metrics are problematic: you can work around them, for example, by writing assertion-free unit tests.
Coverage metrics are a good negative indicator, but 100% test coverage is impractical.
Test are code, and you also have to pay a maintenance cost for your tests.
What makes a unit test valuable?
- carefully choose code to test
- use the most valuable tests only
A good unit test:
- has a high chance of catching a regression error
- has a low chance of producing a false positive
- provides fast feedback
- has low maintenance cost
Testing trivial code is not worth the cost.
Decouple tests from implementation details as much as possible.
Spend most of the time on testing business logic.
Styles of Unit Testing
- output-based verification (functional style)
- state verification
- collaboration verification (uses test doubles)
_image from Wikipedia_
Public API is the surface area that you can access from outside a class.
What are the requirements?
- address an immediate goal of the client code
- address that goal completely
Look at the client code: if it uses more than 1 operation to achieve a single goal, the class is leaking implementation details.
Note: Neighboring classes might be aware of implementation details.
Example: the Root Entity of an Aggregate (Domain Driven Design) might know about implementation details of the Entities.
Communication inside a hexagon is implementation detail.
Between hexagons a public API of the hexagon exist (contract).
- functional style: has no state, easy to maintain, offers the best protection against false positive
- state verification: should verify through public API, reasonable maintenance cost
- collaboration verification: within the hexagon lots of false positives; between hexagons more stable
Black-Box Testing Vs. White-Box Testing
- black-box testing: testing without knowing the internal structure
- white-box testing: testing the internal structure
Adhere to black-box testing as much as possible.
Does the test verify a business requirement?
- view your code from the end user’s perspective
- verify its observable behavior
- test data cleanup: wipe out all data before test execution
Unit Testing Anti-Patterns
- private methods: if needed expose the hidden abstraction by extracting a new concept
- expose state getters: test the observable behavior only
- leaking domain knowledge to tests: use property-paced testing, or verify end result
- code pollution (introduce additional code just to enable unit testing)
- overriding methods in classes-dependencies: violates single-repository-principle, instead split functionality into different pieces
- non-determinism in tests: try to avoid testing async code (separate code into async/sync), use Tasks