Unit testing is part of my normal development flow now. For the most part, I try to write my code in a “TDD” or “BDD” manner, meaning: I write my tests first, explaining how I want my public methods to behave, then follow it up with the actual code to make the tests pass.
Years ago I was convinced that this was the best way to do things, and my own experience has backed up that decision. But, as is the case with many decisions from the past, I’ve started to forget exactly why.
I recently found myself in the role of teaching some other developers my testing practices. When I began, I thought the hardest part would be to explain the technical details: the ins an outs of test runners, mocking, and assertions. While those things are certainly important, as it happened the hardest part was trying to explain why we were writing unit tests at all.
In this particular project I’m writing unit tests for code that I don’t fully understand. As a result, the process of writing the tests (or reading tests written by others) is more about learning. This was my first realization:
Writing tests forces the coder to have a complete understanding of the code they’re testing.
Ultimately, tests have to do with behavior. Given certain inputs, we expect certain output. If I turn the key, I expect the engine to start. That’s a unit test, of a sort: it’s testing the “output” of the engine starting with the “input” of turning the key. But that may also be considered an integration test, because the engine is rather complex, and if my test were to fail, it might be due to any number of factors.
So let’s go one level deeper: If I turn the key, I expect the spark plugs to ignite. That’s another type of unit test, and a much smaller unit. I think that this is the level I usually write tests for. But of course there’s still smaller units within that, all the way down to the physical materials of copper and steel. At some point, you have to trust that some units are just going to work, and write your tests one level above that. This led me to my second realization:
All tests are unit tests, just for differently-sized units. The best unit tests will not only tell you when something is broken, they will also tell you why.
My favorite testing mantra is “red, green, refactor”. If you write your test first and try to run it, it will fail (“red”). Next you write the most basic version of the code to make the test pass (“green”). Finally you can safely refactor the code to be cleaner, as long as its public API remains the same, because the tests will continue to pass (“refactor”). If you want to change the behavior, you start again at “red” by changing the test first. This led me to my third realization:
Testing forces you to consider exactly what you want the behavior to be, and therefore acts as living documentation for how to use your code.
I’m sure there are as many reasons to test code as there is code to be tested, but I’m starting to appreciate my own testing experiences more. I think they’ve made me a better developer, and I hope I’m able to communicate that to others.
Featured image: By Andy Dingley (scanner) (Scan from (1911) Mechanical Transport, HMSO) [Public domain], via Wikimedia Commons