The Long Journey To Clean Tests

It doesn’t take much searching to find a blog post or twitter account preaching the benefits of TDD. One aspect of practicing this discipline that I think is overlooked, as it’s use in a legacy system.

I believe many developers are working in a legacy system, without tests, and are striving to make that legacy system better.

A conversation between a developer in that position and a TDD advocating idealist may go something like this:

Me: “These tests are difficult to write and they take a long time to run.”

Idealist: “Sounds like your code was written poorly. Your unit tests should not depend on outside dependencies like the database and web service calls. It should only test the business rules.”

Me: “I’d agree with that, but the functionality I need to test is not written that way.”

Idealist: “That should tell you something. You need to refactor to decouple your business rules from those dependencies.”

Me: “We’re making progress on that, but it’s not so easy. Some of our business logic is coupled with the database. We’ll need to rewrite all of our queries. To inject the dependencies, we would need to change our API. By changing the API we need to make a lot of changes to the client code. A LOT of changes to the client code.”

Idealist: “You should rewrite the functionality as a separate class or method. That way you can test it. When you are sure it’s working you can gradually make replacements to the client code by switching to the new class/method.”

Me: “We’re trying that, but it takes quite a bit of development to create those new objects. For example, we are writing the persistence logic as a repository and injected it. How do you recommend we go about testing the client code? Most of client code does not have automated tests either. Some of the client code is used by other teams, and they are used to it working correctly. It’s hard to justify the manual testing for functionality that currently works. And did I mention it’s a lot of client code?”

Idealist: “You have to start somewhere, or else you will be testing manually forever. You should write an automated test for a piece of the client code. Once it passes, change the code slightly toward what you are hoping the API looks like. If the test fails, make a modification so that it passes, but only just enough to make it pass. Keep refactoring while keeping the tests green. Do this enough times until the functionality is decoupled”

Me: “We’re trying, but the tests are difficult to write, and they take a long time to run.”

The point is, that the journey to having clean, fast running tests may begin with writing slow and ugly tests. Tests that are slow because they are testing tightly coupled code. The slow, coupled tests still play a vital role: making sure all the current functionality still works and providing the freedom to refactor.

TDD evangelists would agree, the freedom to refactor is key. When you don’t have it, the continuous improvement loop is broken. The problem is, in a legacy system, getting the feedback loop in the first place can be quite the struggle.