I learned about the Unit Testing book through Saša Jurić’s Clarity talk. The entire talk was brilliant but the last 15 minutes especially, when he turned the discussion to testing, were eye-opening. Jurić attributed his style of testing units of behavior instead of units of code to Vladimir Khorikov’s Unit Testing book, so I decided to buy a copy.
I had the book but, truth is, I didn’t plan to read it. It didn’t even make it to my software bookshelf post. After all, I knew what worked for me and what didn’t when it came to tests; there sure was plenty to learn from the book, but I had enough to get by. I’d rather spend my reading time on some other book.
But then I started a new job, joining a new team. What I found there was curious: my new colleagues had been maintaining an extensive test suite, they were very disciplined about it, coverage was high, every public function on every module had its own test. And, yet, this was an ineffective test suite. Tests were a lot of work to write, breaking on the smallest of refactors, bugs slipping through the cracks. What’s worse, this wasn’t perceived as a problem; the team didn’t realize they could do better.
I have strong opinions about testing, so I immediately knew what I wanted to change on this project. The problem was that my opinions were that: just opinions—based on experience but ultimately subjective intuitions. And I was the new guy, without reputation credits to spend; I would need something better than my gut feeling to convince the team to change habits, and to justify the effort to my manager. So for a while, I refrained from proposing any changes and started reading the testing book instead.
Right from the introduction, this book proved to be what I was looking for:
This book can help you articulate why the techniques and best practices you’ve been using all along are so helpful. Don’t underestimate this skill. The ability to clearly communicate your ideas to colleagues is priceless. A software developer—even a great one—rarely gets full credit for a design decision if they can’t explain why, exactly, that decision was made. This book can help you transform your knowledge from the realm of the unconscious to something you are able to talk about with anyone.
I come from a mathematical background and strongly believe that guidelines in programming, like theorems in math, should be derived from first principles. I’ve tried to structure this book in a similar way: start with a blank slate by not jumping to conclusions or throwing around unsubstantiated claims, and gradually build my case from the ground up. Interestingly enough, once you establish such first principles, guidelines and best practices often flow naturally as mere implications.
What pleasantly surprised me was that, without trying too hard, this book says a lot about software design. It makes sense when you think about it: if, as the author suggests, we backtrack to the foundation of our discipline, we’ll land on what testing and design have in common: the pursuit of sustainable software.
A good design lends itself to efficient testing—striving for a good test suite helps arrive at a good design. This is not to say that the code should be adjusted to the tests. And is not to say that the tests should be driving the implementation.
This interrelation between design and testing is best illustrated in chapter 7, where the author suggests an ideal structure for the codebase, and shows how to refactor code towards that structure, thus enabling effective tests.
Overcomplicated code should be split into deep domain classes, to be thoroughly unit tested, and wide controllers, exercised by strategic integration tests.
I found this notion interestingly similar to the discussion of module depth from A Philosophy of Software Design:
But where Ousterhout advocates for avoiding shallow modules, Khorikov suggests that there’s a role for such wide (and thin) classes: to orchestrate the pieces involved in any meaningful operation, freeing the domain model to focus on business logic—the program’s essence.
A unit test is an automated test that:
A good unit test has the following four attributes:
For a piece of code to be part of the system’s observable behavior, it has to do one of the following things:
Any code that does neither of those two things is an implementation detail.
All production code can be categorized along two dimensions:
This categorization gives us four kinds of code: