> blog post tdd-and-the-unit-in-unit-testing

TDD and the "Unit" in Unit Testing

Early in my software development career, I stumbled upon what was to me an unknown practice called Test-Driven Development (TDD). Eager to improve my craft, I started investigating this strange way of coding that required me to write tests before the actual code. I was already used to writing unit tests and didn't like it very much, so I thought TDD could give it a little twist and make it more interesting.

However, my initial attempts at implementing TDD were filled with frustration. Each step of the "Red-Green-Refactor" cycle felt like an uphill battle. I began to question not only my understanding of TDD but its practicality altogether. It wasn't until years later that I realized the core of my struggles lay in a fundamental misunderstanding: The definition of the "unit" in unit testing.

The Early Struggles with TDD

The very first hurdle was the "Red" phase. Writing a failing test. The concept seemed pretty straightforward: Write a test for a function that doesn't exist yet. But in practice, it felt unnatural. How could I write a test for something that hadn't been defined? I found myself inadvertently designing classes and methods in my mind before writing the test, which defeated the purpose of letting the tests drive the design.

Moving on to the "Green" phase was initially less daunting. With a test in place, I could implement the minimal amount of code necessary to make the test pass. This step provided a brief sense of accomplishment.

Then came the "Refactor" phase. In the early cycles, there wasn't much to refactor. I had designed the code upfront, so the initial implementations were already aligned with my preconceived design. However, as I progressed and began working on more complex classes, refactoring became quite cumbersome.

Something as simple as extracting code to avoid duplication introduced a dilemma: What should I do with the existing tests that now only passed because of the extracted function? Should I move them? Duplicate them? The theoretical boundaries between the "Red," "Green," and "Refactor" phases blurred, and the process became chaotic.

This confusion led me to question the efficacy of TDD. It seemed to complicate development rather than improve it. I thought back to times before adopting TDD, when I would write code and then tests, and everything seemed more simple. Disillusioned, I discarded TDD, convinced it was a well-intentioned but impractical methodology.

The Turning Point

Several years later, a recurring issue caught my attention. Whenever I refactored code —moving functions, renaming methods, restructuring classes— I found myself dealing with a plethora of broken tests. These weren't failures due to changes in functionality but because the tests were tightly coupled to the code's structure.

I began to realize that this wasn't just a TDD problem; it was a fundamental issue with how I was writing tests. The tests were brittle because they were too closely tied to the implementation.

Redefining the "Unit" in Unit Testing

The breakthrough came when I encountered a concept that challenged my understanding of "unit" testing. I had always assumed that a "unit" referred to the smallest testable part of my codebase —every single function that could be invoked by a test. But this perspective was limiting and, in many cases, counterproductive.

Instead, I learned that a "unit" could be defined as the smallest piece of testable behavior in a system . This subtle yet profound shift meant that instead of focusing on testing individual functions or methods, I should focus on testing the behavior they collectively produce.

Applying the New Definition to TDD

With this new understanding, the "system under test" became more flexible. It wasn't necessarily every publicly accessible function I could find, but could be any coherent piece of functionality. Defining the system under test became a deliberate decision based on the context and the behavior I wanted to validate.

A Fresh Take on the Red-Green-Refactor Cycle

With this new perspective on what a "unit" truly means, I decided to give TDD another shot.

Writing a failing test, as the first step, became less about predicting the future shape of my design and more about defining the desired behavior of the system. Instead of sketching out the implementation in my head, I asked myself, "What should this feature do?" Focusing on the behavior allowed me to write tests that were meaningful and aligned with the actual needs of the application.

However, the "Green" phase became surprisingly more complicated. It was no longer just about adjusting a single function to make the test pass. Since the tests were now centered on behaviors that spanned multiple functions or modules, making them pass often required changes across different parts of the codebase. This added complexity made the "Green" phase more challenging than before. Yet, this complexity ensured that the solution genuinely reflected the desired behavior of the system, rather than just fulfilling the requirements of an isolated function.

Regarding the refactoring stage, it used to be a minefield where any change could break a web of tightly coupled tests. But now, because my tests were anchored to behavior rather than specific implementations, I could refactor with confidence. As long as the external behavior remained consistent, the internal changes would never affect the test outcomes. This newfound freedom made the refactor phase less about firefighting and more about genuinely improving the codebase. And it felt liberating.

Conclusion

Just by changing my perspective on one concept, the doors of TDD swung wide open, and Suddenly, this part of the development process that once felt like a chore became a creative endeavor and made my job considerably more enjoyable.

In my years of experience, I've seen way too many developers who, like me, didn't really understand the value of a well-written test, and if you are one of them, I hope this article gives you hope that there's a different way, and writing tests can be a rewarding development experience.

Finally, I would like to thank Chat-GPT (if you didn't already notice it) for helping me put my ideas in order, improve my caveman English, and make this article more enjoyable (I guess). I for one, welcome our new AI overlords.