There’s a rumor circulating in agile engineering circles that the “unit” referred to in “unit test” is the unit of production code isolated for testing. Interestingly, that wasn’t what many meant by “unit” when the term “unit testing” first started being used.
Before SUnit (SmallTalk Unit, the first unit testing framework), there were already automated tests. Quality assurance engineers, for instance, often scripted tests in an effort to automate manual parts of their work.
The problem is, it was quite common to see order-dependency in those test suites. In other words, a test couldn’t run on its own, because it needed all of the tests before it to run first so that the system would be put into a specific state that the test would then act and assert on.
This led to all kinds of problems, as you might imagine.
Thus, one of the primary goals of SUnit was that each test could run in isolation from all of the other tests. In other words, a “unit test” is a test that’s isolated from all of the other tests in the test suite. The “unit” is the test itself! (Watch this interview with Ian Cooper to learn more about this).
But today, it’s quite common to see a very different definition of unit testing proffered and propagated. Many engineers believe that for every class, and for every public method of every class, they must create a corresponding “unit test.” Indeed, they believe that the “unit” is the production code—and that therefore each and every public method of the production code is a unit that must be tested.
The Problem
But here’s the problem with that definition of unit testing: you’re no longer focused on testing behavior, but implementation. The tests you write are tightly coupled to the underlying design of your code. Your tests know about every single class, and every single public method of every class. They know about all of the design patterns you’re using.
So why is that a problem? Because the design is constantly evolving. Every single new behavior you add to your system will challenge the underlying design of your code. It will challenge all of the assumptions in your designs. Your designs couldn’t predict the future. And now that the future is here, you have to refactor your designs.
But when your tests are tightly coupled to those design patterns, you now not only have to refactor the designs of your production code—you have to change your tests too! In other words, your tests are making it harder to refactor the underlying designs your code used to satisfy the behavior required of the application. Instead of giving you the freedom to refactor your code, they’ve made it harder to refactor. (You can see a somewhat hyperbolic example of this in the blog post “React License Woes: How To Protect Your Codebases From Churn”).
Furthermore, as engineers build new functionality, and begin to refactor out new classes in their production code while holding the behavior of the system constant, they will often go create tests that map directly to those new classes—and all of the methods on those new classes! Even though the behavior of the system didn’t change during the refactoring, and even though they already had tests covering all of the behavior, they created all new tests anyways. This is an incredibly painful process, that’s even led some to abandon refactoring as a practice!
Focus on Testing Behavior
But instead of abandoning refactoring, or TDD, all you need to do is free yourself from the mistaken definition of “unit testing”, and instead focus on testing behaviors. Instead of saying “for every class, and every public method of every class, I will create a corresponding unit test,” say “For every component (i.e., jar, gem, egg, etc.), and for every behavior of every component, I will create a corresponding unit test.” In this way, you will minimize the surface area of your production code that’s exposed to your test suite. As you refactor your underlying designs while holding behavior of your components constant, you won't need to change your tests at all. Your tests will give you the freedom you need to refactor your code, so that you can keep it clean and GO FAST FOREVER.