🚀 TDD, Where Did It All Go Wrong (Ian Cooper)

Ian Cooper’s talk “TDD, Where Did It All Go Wrong” argues that Test-Driven Development has been misapplied by focusing on testing implementation details rather than system behaviors, leading to brittle, hard-to-maintain test suites. He advocates for a return to TDD’s original intent: writing tests that specify and verify external behaviors at the module or API level, enabling safer refactoring and more maintainable code.

Ian Cooper’s talk, “TDD, Where Did It All Go Wrong,” explores the evolution and misapplication of Test-Driven Development (TDD) in the software industry. He begins by reflecting on his own journey with TDD, noting that while early adopters were enthusiastic, over time, many teams found themselves burdened by large, hard-to-maintain test suites. Cooper observes that TDD, as commonly practiced, often leads to slower development, resistance from experienced developers, and tests that are more of a hindrance than a help, especially when refactoring code.

A central theme of the talk is the distinction between testing implementation details versus testing behaviors. Cooper argues that the original intent of TDD, as described by Kent Beck, was to write tests that specify and verify the external behaviors of a system, not its internal workings. However, many teams have fallen into the trap of writing tests for individual methods or classes, tightly coupling tests to implementation details. This approach makes refactoring difficult, as changes to the internal structure of the code frequently break tests, even when the system’s behavior remains correct.

Cooper revisits the foundational TDD cycle: red (write a failing test), green (make the test pass quickly, even with ugly code), and refactor (improve the code without changing its behavior). He emphasizes that the refactoring step is crucial and often neglected. During refactoring, developers should not write new tests for internal classes or methods; instead, tests should remain focused on the public API or module interface. This separation allows for safe refactoring and keeps tests resilient to changes in implementation.

The talk also critiques the overuse of mocking and dependency injection containers, which often stem from a misunderstanding of what should be tested. Cooper advocates for testing at the module or API level, using mocks only when necessary to isolate expensive or shared resources, not to isolate every class. He introduces architectural concepts like ports and adapters (hexagonal architecture), suggesting that tests should target the boundaries (ports) of the system, ensuring that the core logic remains decoupled from external concerns and is easier to test and maintain.

Finally, Cooper addresses the limitations of acceptance testing tools like Fit, Cucumber, and SpecFlow, noting that while they aim to bridge the gap between requirements and implementation, they often become a maintenance burden and are rarely valued by customers. He concludes that the key to effective TDD is to focus on behaviors, not implementation details, and to use tests as a tool for specifying and verifying what the system should do, rather than how it does it. By returning to these principles, teams can regain the benefits of TDD—maintainability, confidence in refactoring, and faster feedback—without the pitfalls that have led many to abandon the practice.