Technical Debt and Cruft
There is a fundamental tension in software development between delivering something quickly now, and being able to deliver something quickly later. Over time, quick and dirty hacks pile up, and code becomes difficult to work with.
Even if you practice test-first coding, your tests may be badly written, and the code can be badly written, even if they produce the correct answer.
- The test was copied and pasted from an existing test and then hacked, but you didn’t bother to DRY up the duplication.
- The API doesn’t lend itself to pre-test setup because you wrote the code before the tests or failed to refactor the API when you found you had to write ugly test code in order to test it, and so your tests incorporate large chunks of setup code.
- The setup for a test is slow because you have to load a lot of data or do a lengthy computation before you can check to see if the result is correct.
All of these mean that at some point you cut corners on refactoring. Ugly test setup code isn’t The Simplest Thing That Could Possibly Work.
Having a fast unit test suite is a technical requirement that you may have forgotten to build into the test suite. Your unit test suite should fail if all of the individual tests pass but it takes more than 30 seconds total. If it takes longer, you don’t get to see if your recent changes worked, you rapidly exit the Flow state, and productivity plummets.
(Note: The integration test suite, which should be running on a separate continuous integration server, can take a lot longer since it’s not in the innermost loop of the Test Driven Development process. Just don’t mix slow-running full-stack integration and acceptance tests in with your fast, isolated unit tests.)
Keep in mind that it’s okay to refactor code that satisfies customer requirements, but is hard to test with a reasonable amount of setup, or takes too long to test.
Accumulated technical debt has a three-pronged attack on team productivity:
- Features take several times longer to implement than they would with well designed, clean code.
- It becomes harder to estimate how long a seemingly simple feature will take to implement.
- Good programmers get stressed out and discouraged because they know they are performing far below their ability, and they quit. Bad programmers who are used to producing slow, buggy code while missing deadlines will hang around.
…and all of these are true even if you are doing test-first development!
My own mistake in this area has been to allow too much input into the ebb and flow of source code entropy by non-programmers. Managers percieve code quality as if it were a programmer luxury like a foosball table or a team party, that can be taken away when the schedule is tight. It isn’t.
In short, just don’t offer to do shoddy work now in exchange for a repair period later. You’re obligated to keep the code (including tests!) in excellent condition, as a matter of professional competence. You achieve this not by Gold Plating (satisfying your ego by trying to predict features that will be needed and building them in now; see YAGNI), but by keeping the code simple and clear.
Maybe there’s an exceptional case where there’s a super important demo deadline to a Gold Owner, and you need to hack something in as fast as possible by Tuesday. That must be followed by a symmetrically urgent cleanup effort starting on Wednesday. Be firm. The longer you wait to clean it up, the harder it will be: it’s a lot easier to stand firm and insist on a day of cleanup than a month of cleanup.
Non-programmers will never be in a position to realize that this week we should just stop implementing new features and Sharpen the Saw. So, don’t let a week of cleanup work pile up. Just explain that the feature is barely working and demoable, but that it isn’t finished yet. Then clean it up, and declare it done when it really is done. If you have to start a half dozen user stories and then leave them all “in progress” while you do the demo, and then clean all of them up because they’re all in desperate need of additional tests and refactoring, then do that. Whatever you do, don’t create a pile of Engineering Tasks for “go back and write tests for [feature]” and “go back and refactor [feature]“. Those tasks will never get done.
If the demand for emergency demo hackery happens often, there’s something seriously wrong with your scheduling and estimation process: why are you expected to drastically outperform your effort estimate? If the feature was so important, why wasn’t it prioritized high enough to be ready for the demo without the desperate hackery?
Remember, the biggest broken promise that developers hear is “we’ll make time to clean this up later.” And usually it’s a promise you’ve made to yourself.