Not long ago, I absolutely hated code testing. Most of the notions and practices generally associated with code testing made my blood boil. Here’s a list of them:
- The notion that testing will make your program bug free (fact: unless your tests are constructed to exhaustively cover the entirety of the domain of your program, they can never prove the program correct).
- The notion that a program is deeply flawed if it doesn’t include tests (no matter how short or well written the program is).
- The notion that you write your tests first, and your program second.
- Having to install, configure and use test runners. And worse of all, have them as a dependency.
- Badges. We don’t need no stinking badges.
I don’t mind anyone thinking or practising any of the above – I really don’t. If you hold dear any or all of the ideas above, I will still love you, and I’ll gladly discuss them with you. What I hated was the idea of applying any of these things to my own code.
What I’ve learned, however, is that code testing is possible without subscribing to any of the above. Here’s how I do testing in my own libraries:
- The tests are located in one single file, with no extra dependencies than those required for the library.
- Instead of using a test runner, I define one or two functions that execute the tests. Then, I just write tests using those two functions. It’s a little bit of work, but far less than learning, configuring and tweaking a test runner. And far more portable, too.
- I write the tests once I have fleshed out most of the code and even some of the documentation. I write the tests looking at both the code and the readme. For every section of the code that validates something, I write corresponding tests. And for every assertion in the readme, I also write a test targeting said assertion.
- The example code in the readme is part of the tests, so that I can ensure that all the code in the readme is running properly.
- All tests are run automatically. If one of them fails, the whole thing stops and an error message is printed. With one command, I know whether all the tested assertions are still true or not.
- Whenever I find a new bug, I fix it and then add a test that targets said condition.
- I also sometimes piggyback benchmarking functions at the end of the test suite.
This approach to testing has made me enjoy testing. Since I embraced this approach, I found that:
- Bugs are found faster.
- I can reduce the “hand testing” of the code to a minimum – often zero.
- I feel much more inclined to refactor the code when needed. The reason is that I can very quickly check that the code is working exactly as before the change – and if it breaks, I know exactly where.
There’s nothing new about this approach to testing and I’m pretty sure there’s a lot of people out there using this approach. The takeaway is that if you hate the commonly held beliefs and practices of code testing, that needn’t deter you from doing testing in your own way.