How to do TDD correctly?

Integration tests are a scam and in fact most people seem to say that a common problem is that fact we have too many of them and too few unit tests.

First of all J. B. Rainsberger has become a lot less pugnacious about the issue recently which I think comes through in

J. B. Rainsberger - The Well-Balanced Programmer

where he arrives at his own version of Alistair Cockburn’s Hexagonal Architecture (and Kevlin Henney has some words on dependency inversion).

My interpretation of his discussion about his Universal Architecture is:

  • Tests staying entirely in the “Happy Zone” (HZ) aren’t integration tests.
  • Only interactions with the “Horrible Outside World” (HOW) and the “DMZ” are mocked.
  • By design HZ → DMZ dependencies are to be avoided and replaced with (HZ → interface) ← DMZ
  • By design HZ → HOW dependencies are to be avoided and replaced with (HZ → interface) ← DMZ Adapter → HOW. (A.K.A. Narrowing API/Pattern of Usage API.)
  • Integration tests are necessary for:
    • HZ → DMZ (avoid at all cost)
    • HZ → HOW (avoid at all cost)
    • DMZ → HOW
    • DMZ Adapter → HOW
  • By design you want the DMZ and the “Narrowing API/Pattern of Usage API” to be as small/narrow as possible.

i.e. effective testing is about dependency management which is a design activity.

Gary Bernhardt’s FauxO (a play on OO) in Boundaries also relates to this. Given the functional core, imperative shell partitioning, any tests only running any functional core code, no matter how much code that may be, isn’t considered an integration test.

Tests aren’t an end in themselves - the way I measure their value is whether or not they enable “(fearless) refactoring‡ with confidence” while at the same time not getting in the way of refactoring.

  • A test against a published interface (IEEE Software: Public versus Published Interfaces) ensures that I’m not breaking things while I’m refactoring and it’s not going to get in the way because a published interface shouldn’t change anyway.
  • A test concerned with implementation details is going to get in the way of refactoring when refactoring is changing the implementation (implementation isn’t the same as behaviour).

For me the core value has always been to design a system to be testable so that I can always refactor with confidence - i.e. testability (for the purpose of refactoring) being a core value of good design. While tests can help you discover a design that is easier to test, that doesn’t mean that testing will inevitably lead to a good design.

‡to reduce volatility in the marginal cost of features.

6 Likes