A pretty-darned-optional part of being a software developer is getting a generally-applicable principle (a “law”) named after yourself. Most of the time, it seems, this happens unintentionally–the law’s originator doesn’t necessarily foresee it becoming a pithy and oft-repeated internet adage. But if I could choose a principle to call “Lyman’s Law,” I think it might be this one:
Code accompanied by unit tests will have a higher standard of code quality than code written without unit tests, even if the unit tests themselves are not taken into account.
If someone else has claimed this law already, feel free to point me in their direction so I can give them credit.
This is an argument for writing unit tests–or requiring your team to write unit tests–in situations where you don’t feel that the unit tests themselves will add much value. Why? Because code written for testability is usually more modular, more loosely-coupled, more interface-driven, and more composable than code written for suitability alone.
You may feel this goes without saying. Isn’t that the raison d’être of unit tests? Weren’t they invented to improve our code? Well, yes, but you’re probably confusing code quality with code correctness. That unit tests catch bugs and prove the correctness of your algorithms is a foregone conclusion. What I’m talking about is completely separable from that. Unit tests improve the non-functional aspects of your code: the maintainability, the readability, the modularity.
This has become significant to me as I’ve worked on a legacy codebase written without testability in mind. All the data is stored in a giant global object, no code is shared, there are reams of dead code lying about, UI and logic are tightly coupled, and much of the logic is nested seven or eight levels deep. In short, the Cthulhu of software projects. It’s occurred to me more than once that one of the best ways to avoid this catastrophe would have been to instruct the team who built it to write unit tests. Arguably, they could have written bad unit tests and left the code in much the same state it’s currently in. But it would have taken so much more cognitive effort to do so than to break down and modularize their code that one would hope they’d choose the latter.
If you’re feeling so inclined, I’m sure you could provide a code snippet showing me that testable code can still be terrible code. You’re not wrong. But untestable code cannot be good code. Therefore, unit-tested code will generally be of higher quality than untested code.
If you’re incredulous about this claim, here’s a thought exercise: ask yourself what traits may make a class or method untestable. Then determine what relationship these traits have with generally-accepted principles of code quality.
You may say that there’s a difference between untested code and untestable code. I say the difference is mostly academic. If testability is not an explicit goal of the code, the developer will almost always take the path of least resistance (or, at the very least, make several small mistakes) and build in a way that makes it impossible or prohibitively difficult to add unit tests without refactoring.
To summarize my thesis: unit tests love code quality. The easiest thing to test is a loosely-coupled pure function with predictable outputs and a simple interface–and all of the above are hallmarks of clean code.
If you have something to add, please share your thoughts in the comment section.