your unit tests are telling you something
Abstract
When should I refine the design? Writing tests give you hints on when to improve the design.
Rules of simple design by Kent Beck:
Tests give us hints on how to break complexity:
- vertically
- horizontally
- by roles
Mocks
Why we live mocks:
- isolated tests from external systems
- fast (no framework, db, external APIs)
- simpler when testing high complexity
Cyclomatic complexity = number of independent execution paths through code = max number of tests.
Why we hate mocks:
- uncaught bugs
- fragile tests that break when refactoring codes
- unreadable tests
Testing pyramid vs honeycomb testing strategy
Testing pyramid is for the bad monolith (“big ball of mud”) which has:
- highly coupled code
- huge complexity behind a few entry points ⇒ the need for lots of unit tests
Testing microservices / modulith is different.
- huge complexity in a single microservice = bad practice ⇒ break microservice / module
- many APIs, hiding a decent amount of complexity
- easier to test more at the API level ⇒ honeycomb testing strategy
- integrated
- integration
- implementation details
- test roles, not methods / classes
- write social unit tests
Test manageable complexity without mocks, instead, test “components” (group of objects) ⇒ internal refactoring won’t break tests.
Constrained objects
Constrained objects = data structures that guard their internal consistency by throwing exceptions (e.g. required fields, string size, …)
- mutable (e.g. domain entities, aggregate)
- immutable (value objects)
Creating valid test data for a large constrained object is cumbersome.
- test the entire group with a social unit test requires larger setup and larger input ⇒ object mother pattern, i.e. a shared class creating valid test objects
- same test data factory used in different verticals
- introduces coupling, hard to change
- ⇒ break object mother per vertical
- break domain entities in separated bounded contexts
- packages > modulith > microservices
Unit tests speak your domain model. Unit tests are first class citizen of your project. Respect them. Unit testing loves:
- precise signatures
- smaller data structures
- agnostic domain
Unit testing puts design pressure on highly complex logic.
Fixture creep
Complex methods in the same class use different sets of dependencies, e.g.:
Then you might have to mock
A
andB
. Mitigation: split unrelated complexity: horizontal split class. In this example, we could have aWideA
that contains onlyA
andWideB
that contains onlyB
.Mock roles, not objects
⇒ contract testing Before mocking a dependency, clarify its responsibility. Changing an API you mocked is painful.
Functional programming
Unit testing promotes functional programming:
- pure functions
- has no side-effects
- same inputs ⇒ same outputs
- just compute value
- no network or file
- no changes to data
- no time/random
- immutable objects
If you have a very complex logic using many dependencies (e.g.
computePrice
,applyDiscounts
), move the complex part in another class, i.e. reduce coupling of complex logic.move complex logic in pure function:
It will be easier to test with less mocks.
Imperative shell / functional core segregation
Move complex logic in the functional core. All the imperative shell shall be used for dependencies, state mutation, API calls, DB, files, …
Temporal coupling
You use mutable objects, so by swapping two lines can still cause bugs in production. This might lead to paranoid testing, i.e. verifying method call order.
Instead, use immutable objects.
Design hints from tests
- collapse middle-mand vs “What am I testing here?” syndrom
- honeycomb testing strategy vs fragile microscopic unit tests
- precise signatures
- tailored data structures vs creepy object mother
- keep complexity inside agnostic domain, not on APIs or extensive libraries
- separate complexity by layers of abstraction (vertical split) vs
@Spy
- separate unrelated complexity (horizontal split) vs fixture creep (bloated setup)
- refine roles (mock roles, not objects) vs blindly
@Mock
all dependencies- more complexity ⇒ less dependencies vs mock-full tests
- promote immutable objects vs termporal coupling