I wrote a lot about Test Driven Development back in the days of the now defunct CodeBetter site. You can read a little of the old precursor content from this old MSDN Magazine article I wrote in 2008. As time permits or my ambition level waxes and wanes, I’ll be resurrecting and rewriting some of my old “Shade Tree Developer” content on team dynamics, design fundamentals, and Agile software practices from those days. This is just a preface to a new blog series on my thinking about how to effectively do TDD in your daily coding work.
The series so far:
I’m giving an internal talk at work this week about applying Test Driven Development (TDD) within one of our largest systems. Our developers today certainly build tests for new code today with a mix of unit tests and integration tests, but there’s room for improvement to help our developers do more effective unit testing with less effort and end up with more useful tests.
That being said, it’s not all that helpful to just yell at your developers and tell them they should “just” write more or better tests or say that they should “just do TDD.” So instead of yelling, let’s talk through some possible strategies and mental tools for applying TDD in real world code. But first, here’s a quick rundown of…
What we don’t want:
- Tests that require a lot of setup code just to establish inputs. Not only does that keep developers from being productive when writing tests, it’s a clear sign that you may have harmful coupling problems within your code structure.
- Tests that only duplicate the implementation of the code under test. This frequently happens from overusing mock objects. Tests written this way are often brittle when the actual code needs to be refactored, and can even serve to prevent developers from trying to make code improvements through refactoring. These tests are also commonly caused by attempts to “shut up the code coverage check” in CI with tests retrofitted onto existing code.
- Tests that “blink,” meaning that they do not consistently pass or fail even if the actual functionality is correct. This is all too painfully common with integration tests that deal with asynchronous code. Selenium tests are notoriously bad for this.
- Slow feedback cycles between writing code and knowing whether or not that code actually works
- Developers needing to spend a lot of time in the debugger trying to trace down problems in the code.
Instead, let’s talk about…
What we do want:
- Fast feedback cycles for development. It’s hard to overstate how important that is for developers to be productive.
- Developers to be able to efficiently use their time while constantly switching between writing tests and the code to make those tests pass
- The tests are fine-grained enough to allow our developers to find and remove problems in the code
- The existing tests are useful for refactoring. Or at least not a significant cause of friction when trying to refactor code.
- Test tests clearly express the intent of the code and act as a form of documentation.
- The code should generally exhibit useful qualities of cohesion and coupling between various pieces of code
And more than anything, I would like developers to be able to use TDD to help them think through their code as they build it. TDD is a couple things, but the most important two things to me are as a heuristic to think through code structure and as a rapid feedback cycle. Having the tests around later to facilitate safe refactoring in the codebase is important too, especially if you’re going to be working on a codebase for years that’s likely going to outgrow its original purpose.
So what’s next?
I’ve already started working on the actual content of how to do TDD with examples mostly pulled from my open source projects. Right now, I’m thinking about writing over the next couple months about:
- Using responsibility driven design as a way to structure code in a way that’s conducive to easier unit testing
- Some real world examples of building open source features with TDD
- My old “Jeremy’s Rules of TDD” which really just amount to some heuristics for improving the properties of cohesion or coupling in your code based on testability. I’m going to supplement that by stealing from Jim Shore’s excellent book on Testing without Mocks
- A discussion of state-based vs interaction based testing and when you would choose either
- Switching between top down code construction or bottom up coding using TDD
- What code deserves a test, and what could you let slide without?
- Choosing between solitary unit tests, sociable unit tests, or pulling in infrastructure to write integration tests on a case by case basis
- Dealing with data intensive testing. Kind of a big deal working for a company whose raison d’etre is data analytics
- Not really TDD per se, but I think I’d like to also revisit my old article about succeeding with automated testing
- And lastly, what the hell, let’s talk about judicious usage of mock objects and other fakes because that never seems to ever stop being a real problem
I’m happy to take requests, especially from colleagues. But I absolutely will not promise prompt publishing of said requests:)