When TDD goes bad
January 13, 2022
Blog > TDD, Unit Testing | Valentina Cupać
When TDD goes bad. When you feel that TDD is working against you. When you feel like it’s a big waste of time.
You want to make a small change to a method on a class, and then hundreds of UNIT TESTS break. This means you have to spend MORE time fixing tests than making the actual change? That doesn’t seem rational, right?
You feel that tests are making it HARDER for you to change.
You feel that tests are making refactoring almost IMPOSSIBLE.
You feel that tests are IMPRISONING you, locking you into existing designs.
ROOT CAUSE: The problems above occur when the UNIT TESTS are tightly coupled to code structure (instead of behavior). Unfortunately, this kind of approach (structural coupling) is something that comes from many tutorials, articles, etc. which tell you to create a test per class.
What’s the structural coupling between test code and production code?
– Case 1: High structural coupling between test code and production code. In this case, we create a one-to-one correspondence between test code and production code. So for each production code class, we’ll create one test class. For each production code method, we’ll create test method(s). Here the structure of the test is tightly coupled to the structure of the production code. It causes fragile tests, whereby changes in the design of production code cause any related tests to fail.
– Case 2: Low structural coupling between test code and production code. In this case, the tests are coupled to the API of the application (and not the internal production code). This means your tests are specifying system behavior, without caring about the implementation details; the tests are not coupled to the internals of the system. So refactoring production code doesn’t affect the tests, only changes in the API affect the tests.
What’s the impact on design?
– Case 1: Design of production code becomes rigid. The tests have HIGH granularity (bound to structures with one-to-one correspondence). It’s too expensive to refactor production code. The tests have a high maintenance cost due to the high coupling.
– Case 2: Design of production code can evolve. The tests have LOW granularity (bound to behavior but not to structure). Refactoring can be done without breaking tests. The tests have low maintenance cost due to the low coupling.
TDD only enforces the testability aspect of design, but all other attributes of design quality are up to the developer themselves. TDD neither creates good designs nor bad designs. The developer makes the design decision.
Do tests constrain design?
– Case 1: The tests constraint design choices (i.e. if you change the design, you have to change the tests)
– Case 2: The tests do not constrain design choices (i.e. if you change the design, tests are not affected unless there’s a change in the API).
Ref: See Uncle Bob’s article “TDD Harms Architecture”