I discussed many benefits of using test-first development over these last eight blog posts. I find many benefits to doing test-first development but one stands out to me to be the biggest benefit and that is that the tests that I write concretize abstract requirements. This makes requirements understandable because they are real and tangible and associated with specific behaviors that I can assert against. It’s amazing how much ambiguity just dissolves away when we have the clarity of good unit tests that draw on accurate examples.
I want the examples that I use in my unit test to reflect how the system will be used so that I have an accurate representation of how I’m using the system in my tests. We understand so much better through examples. We think in concrete terms but we speak in generalizations. When we talk and think in generalizations then we’re constantly concretizing those abstractions in our heads in order to make sense of what is being said. Using concrete examples to illustrate concepts increases the fidelity of communication and helps everyone get on the same page to understand each other.
We always want to have a verifiable system that we can test against. I remember that I was working with one team that basically hardcoded all of their calls to the database which meant that their system was not very testable. They could write a test that simulated a user of a system but they couldn’t write small unit tests that could fail for only one reason that they needed in order to have a really good continuous integration system.
When we started trying to write unit tests for their system we found that we couldn’t mock out the database calls directly without breaking the system. Instead, they had to start by writing tests against a test database knowing that their next step once they got that working was to leverage it so that they could refactor their code again and replace the test database with mocks so that they could write good unit tests and have a responsive continuous integration server.
As much as we wanted to go directly to having good unit tests and mocks, we found that we couldn’t do it without bringing the system down for weeks and you never want to be in that situation because it could be very hard to get the system back up again. All changes that we make to a system, especially a live system, have to be small, safe, and incremental. If we can’t compile or run our code for long periods, it’s like trying to code blindfolded. We don’t want to do that.
When we’re doing test-driven development or acceptance test-driven development we are essentially defining the behavior of our features with examples and this is an extremely valuable and useful way to build software. I think it is the best way of building software that I have encountered and that is why I am an advocate. Of course, if we’re going to build a system based upon examples then we want to make sure that our examples are accurate and make use of the system the way we intend the system to be used in production.
One of the many benefits that I find when doing test-first development is that I can build components of the system and exercise them in the way I intend to use them in production so that I have another way of validating that they work the way I intend them to. When I’m doing test-first development all of the code that I write gets called in two ways. My code gets called from production and my code gets called from my tests. I want to call my code from my tests and use it the way I intend for my code to be used in production.
Note: This blog post is based on one of the “Seven Strategies…” sections in my book, Beyond Legacy Code: Nine Practices to Extend the Life (and Value) of Your Software.
Previous Post: « Avoid Over-Specifying Tests
Next Post: Why Practice Eight: Implement the Design Last »