Thursday, December 25, 2014

Why we Test, part 2: Design

Previously, I talked about the perspective that the reason to write tests is to catch bugs, and this is a good thing all around. Now I want to talk about code design - about using tests to help me design my code well. Some people argue this is the "true meaning" of "Test-Driven Development".

Unit tests can point me towards good design: if its difficult to write a good test, it means the code is poorly factored, especially that the thing I want to test is inappropriately coupled with something I don't care about right now. Introducing indirection at this point can open my code up to a valuable abstraction.

The "good test" that is "easy to write" will look something like:

    testSubject = new Foo(/*initial state*/);
    result = testSubject.Bar(..);

That's Arrange-Act-Assert, with one line of each. No need to write comments to that effect; no need for blank lines. (There are a few other similar forms in the 2-4 line range.)

This kind of test is only possible if the code under test is not tightly coupled to the rest of the system. More than just "programming to interfaces" so I can inject dependencies, I look to eliminate dependencies. I don't have much setup code. I don't use mocks because I don't need to.

It's not just about "testing a class", it's also about "testing a business rule". If I can test my business rules this way, they are DRY: each piece of knowledge has exactly one canonical expression in my codebase. I minimize emergent phenomena, so my whole system is easier to reason about.

Since I have unit tests that express my business rules, I use terminology from my business domain in the tests. That terminology will flow in to my system-under-test, giving rise to ubiquitous language (within my bounded context of course.)

This kind of test will naturally be super-fast and completely reliable, which supports the "catch bugs" value described before. But I also write a lot fewer bugs, because I have well-factored, well-named, decoupled, DRY code that is easy to reason about. Writing fewer bugs is more effective than trying to find-and-fix the bugs with tests.

I can treat bugs as another kind of design feedback: I can ask what made it easy for this bug to appear, and look for a way to eliminate the whole class of bugs. I may use a unit test for this purpose, but simply writing a test for the specific bug is not enough - I want to address the whole class.

Refactoring is still really important, and (unless I have great tools in C# or Java) I must count on the tests to protect me while refactoring. But now I have the advantage that a) my code is relatively well-factored already, and b) my tests are helping me figure out good ways to refactor, so refactoring is much more fruitful.

You only get this value if you listen to the feedback your tests are giving you.

This value appears when tests are written.


Philip Schwarz said...

From Forgotten Refactorings ( "don't touch anything that doesn't have coverage. Otherwise, you're not refactoring; you're just changing shit."

Philip Schwarz said...

I can't believe these 2 panels did not debate the idea that TDD turns testing into a design activity

Back in May 2014, when Martin Fowler asked for suggestions for topics for round two of the first panel ("Is TDD Dead"), I tweeted the following at @dhh @martinfowler @KentBeck @unclebobmartin @sf105 @natpryce:

before debating the fallacy that "an easier to unit test system is a better designed system" and that "you can only prove your design by making it more testable" why not involve Steve Freeman and Nat Pryce who say (in GOOS): "We write our tests BEFORE we write code. Instead of just using testing to verify our work after it is done, TDD turns testing into a DESIGN activity. We use testing to clarify our ideas about WHAT we want the code to do. As Kent Beck described it to us, 'I was finally able to separate logical from physical design...'. We find that the effort of writing a test first also gives us rapid feedback about the quality of our design ideas - that making code accessible for testing often drives it towards being cleaner and more modular."

That had no effect. I was hoping that Nat Pryce's presence on the second panel would make a difference, but no.