I’ve heard that projects developed using TDD are easier to refactor because the practice yields a comprehensive set of unit tests, which will (hopefully) fail if any change has broken the code. All of the examples I’ve seen of this, however, deal with refactoring implementation – changing an algorithm with a more efficient one, for example.
I find that refactoring architecture is a lot more common in the early stages where the design is still being worked out. Interfaces change, new classes are added & deleted, even the behavior of a function could change slightly (I thought I needed it to do this, but it actually needs to do that), etc… But if each test case is tightly coupled to these unstable classes, wouldn’t you have to be constantly rewriting your test cases each time you change a design?
Under what situations in TDD is it okay to alter and delete test cases? How can you be sure that altering the test cases don’t break them? Plus it seems that having to synchronize a comprehensive test suite with constantly changing code would be a pain. I understand that the unit test suite could help tremendously during maintenance, once the software is built, stable, and functioning, but that’s late in the game wheras TDD is supposed to help early on as well.
Lastly, would a good book on TDD and/or refactoring address these sort of issues? If so, which would you recommend?
I do agree that the overhead of having a unit test suite in place can be felt at these early changes, when major architectural changes are taking place, but my opinion is that the benefits of having unit tests far outweigh this drawback. I think too often the problem is a mental one – we tend to think of our unit tests as second class citizens of the code base, and we resent having to mess with them. But over time, as I’ve come to depend on them and appreciate their usefulness, I’ve come to think of them as no less important and no less worthy of maintenance and work as any other part of the code base.
Are the major architecural ‘changes’ taking place truly only refactorings? If you are only refactoring, however dramatically, and tests begin to fail, that may tell you that you’ve inadvertantly changed functionality somewhere. Which is just what unit tests are supposed to help you catch. If you are making sweeping changes to functionality and architecture at the same time, you may want to consider slowing down and getting into that red/green/refactor groove: no new (or changed) functionality w/o additional tests, and no changes to functionality (and breaking tests) while refactoring.
Update (based on comments):
@Cybis has raised an interesting objection to my claim that refactoring shouldn’t break tests because refactoring shouldn’t change behavior. His objection is that refactoring does change the API, and therefore tests ‘break’.
First, I would encourage anyone to visit the canonical reference on refactoring: Martin Fowler’s bliki. Just now I reviewed it and a couple things jump out at me:
In light of this, if a test or tests has to change during a refactoring, I don’t see this as ‘breaking’ the test(s). It’s simply part of the refactoring, of preserving the behavior of the entire code base. I see no difference between a test having to change and any other part of the code base having to change as part of a refactoring. (This goes back to what I said before about considering tests to be first-class citizens of the code base.)
Additionally, I would expect the tests, even the modified tests, to continue to pass once the refactoring is done. Whatever that test was testing (probably the assert(s) in that test) should still be valid after a refactoring is done. Otherwise, that’s a red flag that behavior changed/regressed somehow during the refactoring.
Maybe that claim sounds like nonsense but think about it: we think nothing about moving blocks of code around in the production code base and expecting them to continue to work in their new context (new class, new method signature, whatever). I feel the same way about a test: perhaps a refactoring changes the API that a test must call, or a class that a test must use, but in the end the point of the test should not change because of a refactoring.
(The only exception I can think of to this is tests that test low-level implementation details that you may want to change during a refactoring, such as replacing a LinkedList with an ArrayList or something. But in that case one could argue that the tests are over-testing and are too rigid and fragile.)