Developing software is a complex activity. Having worked with thousands of professional software developers, I recognize that there are many ways to implement any set of requirements and many are equally valid. This is true at least from the computer’s perspective but from the human perspective, we favor software that’s straightforward to understand and modify because the vast majority of costs in software have to do with extending it later.
For me, dropping the cost of development and ownership of the software we build is of paramount importance. Agile software development requires paying attention to what we’re doing when we build software, paying attention to the design, and paying attention to new and changing requirements. In my opinion, this is a far more disciplined approach than following a checklist as we did in Waterfall software development and really tuned out to everything but an old, out-of-date requirements document.
In Agile, we want to learn as we go and we want to build systems incrementally so that we can see them emerge and evolve. The most valuable way I have found to do this is through test-first development. Doing test-first development is an advanced engineering practice and I’ve seen it implemented in many different ways in the industry, not all of which are effective. Misko Hevery says that many developers think they know how to write a good test but they really don’t, and I have similar experiences.
Good tests are actually not that easy or obvious to write but they are very important, not only for doing test-first development correctly but for contextualizing our thinking about the behaviors that we want to build in ways that are effective and independently verifiable.
I am a huge advocate for test-first development but not as a replacement for doing quality assurance. I think that these are totally different activities and I see developers misapply test-first development by thinking it’s some sort of QA activity. This drives them to write too many tests and implementation-dependent tests that break when they go to refactor their code.
One of the main benefits of doing test-first development is having a good set of behavioral tests that we can use to verify that features work as expected. These tests should be designed to allow us to refactor our code safely. I want my unit test to have my back so that if I accidentally change the behavior of a feature when refactoring it, my tests will tell me by failing. I don’t want my test to break when I go to refactor my code because they’re brittle and implementation-dependent. That doesn’t help me, it slows me down because now I have to fix tests as well as code when I refactor my design.
I see a lot of confusion out there around writing unit tests but I find that the kind of tests that we write when doing test-first development is of paramount importance. They help us not only with regression but also by helping us define and implement cohesive and decoupled behaviors in the system. This is the core of a successful continuous integration system that supports DevOps and drops the cost of building software.
If we simply think of the tests that we write when we do test-first development as a way of eliciting or expressing the behaviors in the system that we want to create, then we can start to use the language of our tests to assert the behaviors that we want in a system. It actually turns out that this is a very straightforward way of writing tests and it gives us guidance on how to write tests for any behavior that we can think of.
That’s the idea in a nutshell. Instead of thinking of them as tests, use them to specify the behaviors you want to create.
The following seven blog posts come from a section in my book, Beyond Legacy Code: Nine Practices to Extend the Life (and Value) of Your Software, called Seven Strategies for Using Tests as Specifications. These strategies help us build tests that maximize their value as both executable specifications of the system as well as helping everyone get on the same page with the minimum number of tests required to specify any behavior in a system.
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: « Make Each Test Unique
Next Post: Instrument Your Tests »