Dawid Ciężarkiewicz aka dpc

Be warned: Tests are great calcifiers

Today, I'm going to give a contrarian view on tests and testing in software.

From my experience, the most common view on software testing is: “Tests! Tests! Always have tests! Start with a test! End with a test! More tests – good! Less tests – bad! 100% coverage! Test everything! How can you not have tests everywhere?!” ...

... or its opposite-side tween: “Tests? Why bother? Maybe later.”

Before I go any further, a note for the typical distracted reader, that is going to skim over the text, misunderstand it, and go rage over social media how stupid the author of this post is: I'm not saying that tests are bad. I'm going to focus on the bad side, but that is not the only side. Go read somewhere else about the good sides.

Important cost of testing: calcification

Definition of Calcification:

The process of change into a stony or calcareous substance by the deposition of lime salt; — normally, as in the formation of bone and of teeth; abnormally, as in calcareous degeneration of tissue.

To cut to the chase... Tests are one of the most efficient ways to turn your code from a flexible, malleable living tissue into a hard, impenetrable, dead-like shell.

The only faster way to calcify is to expose a public API. Once it's public and has users, it's very hard to change. Any change risks breaking someone's use-case. Hyrum's Law. And the reason why tests are calcifiers is that they effectively are “users” of whatever they test.

How does it work in practice?

A developer comes and sees something wrong with a code: “Hmm... This is not right... Sub-optimal at best. It used to be like this a year ago, but now we should change it... Let's give it a try... a moment passes... Done, easy. Oops... there are 153 tests that depending on this piece of code... Whatever, I need to focus on the task at hand. Maybe next sprint...”

And so your sub-optimal or even wrong code never gets fixed.

Every test, despite all the benefits it gives, is an additional obstacle to change what is being tested.

How to deal with test-caused calcification

The way I tend to view code-bases is through the analogy of a living organism. When a software project is started it is like a newborn: tiny, immature, insufficient, full of flaws, but very flexible and full of potential. Newborns grow at an amazing pace. Then they gradually become kids – bigger, more self-sufficient, reliable. But they also become more inflexible: both psychologically and physically. You can see their character more clearly, strong and weak sides. They lose their potential, as they become more definitive. When they finally reach their adulthood, they are fully developed: very hard to change, rigid in their opinions and behavior, but also – most powerful and productive. Eventually, they get old and die, but that's a story for another time.

So the first thing is – you have to respect this natural lifecycle. Start your project like a baby starts life – insufficiently tested, fragile and full of flaws, but easy to grow. Add a small number of targeted tests in crucial places that you want to make solid very early. Like the human baby's brain needs to be protected, so that skull bones turn from very soft to hard between 6th and 18th month of life, while rest of the body stays relatively soft and flexible. With time you should harden more and more parts of your software. If you do it too quickly, you risk turning your software into a twisted, underdeveloped dwarf of what it could become if you let it stay flexible and grow longer. If you do it too slow, then your project is going to be needlessly fragile. Like many things in life – testing is a balancing act.

Notice that even a fully developed adult is not like a stone brick – some parts of our body are hard, but most of it needs to stay a soft tissue, for us to be alive. Same with software – you need to figure out which parts of your software have to be hard and solid to build a solid frame, and which flexible and soft to allow adapting to change.

Your tests should calcify and harden only what ought not to change. You have to be really careful to avoid turning living organs into stones. This is another way of stating the well-known rule: “testing implementation details is bad”. Testing implementation details is like calcifying internal organs.

Be opportunistic when testing. There are things that are easy and natural to test – self-contained functions, logic, modules, with well-understood API that can be easily tested in isolation. Consider testing these places first, as it is very cheap.

Test on API level! APIs are hard to change by their very nature. If they are public – they are calcified from the moment they get users relying on them. If they are internal, other pieces of your system are effectively their public users – might be easier to coordinate change, but it's still harder than changing private internals. The damage done by calcification via testing is less severe if you test APIs. Part of the reason why I prefer well thought E2E testing to masses of inconvenient unit-test.

Work hard on modularizing your code. Every self-contained module is a nice soft tissue, usually surrounded by somewhat more solid API-shell. And APIs are great points for testing! The more modularized your code is, the more nice APIs you have to use for testing, without calcifying vital internals. And even if you happen to over-calcify a module or a component, as long as it's reasonably self-contained, you can just throw it whole and replace it with a one flexibly grown from scratch.

Give yourself plenty of room for Moulting. Again – growing, living things analogy. Some organisms, to deal with having to develop hardened parts early, yet allow a room for future growth, just drop the whole parts of their body (eg. outgrown shells). Your software can do it too, but you have to think and plan for it ahead of time. Componentization, composability, message-passing, microservices, event-sourcing, and many other methods are great enablers for “dropping whole body parts” when necessary.

There is such a thing as too many tests.

The final advice: plan your testing consciously! Don't do it mindlessly, the same way, every time, for every project. There is no one-fit-all testing approach. Weight costs to benefits! Understand the mechanism of calcification caused by test and cost of maintaining them, and the benefits of testing, and pick the right combination for your project and business goals.