TDD ≠ Unit Tests

February 7, 2022
The unit of test in TDD is a behavior. That may correspond to a "unit." It often does not.

When I mentioned my recent TDD failure on LinkedIn, it sparked a conversation about TDD vs Unit Tests, in which I made the claim:

The unit of test in TDD is a behavior. That may correspond to a “unit.” It often does not.

Someone in the thread asked me for some examples of what I mean, and here I wnat to provide that.

I imagine most of us are at least conceptually familiar with the test pyramid.

There are many variations, each with its own specific layers, but it’s the concept that matters here.

My point was that when doing TDD, you may write tests that fall anywhere on that pyramid. This often does not correspond to any standard definition of a unit test.

So let’s construct an example.

Imagine we have a web app with a broken login page. If you enter a blank password, the system simply crashes, rather than returning an error.

  • E2E-based TDD The system was built without automated tests. You don’t even have the ability to mock the authenticatoin database. Getting it under test properly is a big task, but you can’t take the time to get there before doing your Red/Green/Refactor stage to fix this bug. So you write an automated E2E test using something like Selenium. You then do your debugging, solve the problem, and now the test passes.
  • Integration-based TDD Let’s say the system is in a little bit better state, but still not easily testible. In this example, you can mock the authentication database in your tests. You still need to launch an instance of the service, connected to your mock database, but at least it’s not the whole system. You write your test. It fails. You do your debugging, fix the bug, and now your test passes.
  • Unit-based TDD In the ideal scenario, you actually have a system that’s been built (or refactored) with testing in mind from the ground up. In this case, you can easily isolate the auth module, and write a simple behavioral test around just this aspect of the system. It fails. You fix the bug, and now the test passes.
  • Sub-unit-based TDD This one is controviersial in some circles. There’s a school of thought that you should only ever test public methods, and while this is good general advice, I believe its appropriateness depends a lot on which language/framework/stack you’re using, and how much flexibility it affords you. There are times when I write tests around private functions or methods. Although admitedly, I can’t imagine this would be appropriate for such a straight-forward auth bug. But you could, potentially, in some circumstances, write a test in the course of TDD that tests a private function or method. We might call this a sub-unit test.

Aside from legacy, untested systems, there are also times when it’s just easier, or even necessary, to do an integration test as part of your TDD cycle. Particularly if you’re in the process of writing an integration.

As another example, Behavior-Driven Development, a particular interpretation/approach of TDD, often exclusively uses E2E or UI tests. These tests are often coupled with lower-level tests, of course.

So in summary, TDD and “Unit tests” are orthogonal concepts. Many TDD tests happen to be unit tests, but that’s almost incidental, not by design or necessity.

Related Content

Test your tests

TDD has a built-in "check bit" to help ensure that your tests are valid.

My Most Controversial Opinions

Why Small Batches Make Us Happy

4 surprising ways that small batches affect us psychologically