Unit Tests breathe soul into your code
20 June 2021
As many of you may know, this blog is a self-discovery process. It is also possible that none of you may know. I am not aware of anyone other than myself reading this. The topics that I generally write about fit the following description - these are ideas that I have encountered in my experience. I do not claim that they are new ideas to the world. Simply that I did not read or listen about them verbatim and discovered them in the wild. I hope that you, the reader, would also enjoy discovering these articles. They’re cute, aren’t they?
I have spent months thinking about writing on this topic and have hesitated. Although I am convinced of the point, I did not find my arguments convincing. What helped me push across the finish line is a little book called The Programmer’s Brain by Felienne Hermans. I have been following her work for a few years now, initially attracted to her work on programming in MS Excel. I was fascinated by functional programming paradigms and wanted to nail down the thought that MS Excel indeed fits into the paradigm.
Enough of the welcome blabber! Let’s jump on then. What are the reasons we write unit tests for our code? One is that it helps identify bugs early. This is why I was taught I must write tests, and I have benefitted from this. However, many seasons later, I have discovered there are more advantages to this. One of them is documentation. Programmers new to the codebase get a lot to learn about the code from the tests. Interpreting complex functions becomes more manageable when one looks at examples of their input and output.
As of writing, I work with data science pipelines in Python, and I’ve chosen to keep my test cases heavily redundant. Although the size of the codebase can be reduced plenty if I bother refactoring my input sets. I’ve made this choice to aid a new programmer to interpret any function with greater ease.
That’s all the point there is. I want to put some frills to these thoughts, though. I tend to think of this in terms of breathing soul into your code. Programming can be described as modelling objects. These could be from the physical world. For example, you are writing a class to represent a car in a racing game. These could be from the abstract world. You could be processing a linked list that represents the file system of your operating system. In all these scenarios, certain implicit assumptions drive the implementations. You don’t write these in the code. Instead, you assume (or hope) that the future programmer would also work with these implicit assumptions. For example, a
Car class would not have a ‘fly’ method. Or would it?
Let us say that you assumed it would not. Let’s fast forward to the future. Your game has a bug that sends the car bury a few centimetres in the ground after a collision. The most obvious test that would prevent this scenario is to check that the y-coordinate of the car cannot be negative. Here, I have assumed y is the vertical axis, and a value of 0 implies the ground. This is fascinating to me. It is one thing to test if your method does what it is supposed to do correctly. It is another to test if it does not do what it is not supposed to do. You can keep things on a chair and you can keep things on a table. What’s the difference? Tests give the objects their identity. This may seem trivial for a class like
Car, but you do not always work with classes with a clear analogue. While the identity of a
Car class may be intuitive to an average programmer, the same would not hold true for the
AppointmentBook class in management software. Who can create an appointment? What does an appointment look like? The answers would depend on the context, which the original programmer would have been aware of. As the programmer who has to implement a change in the future, you need to know the original context. Documentation is helpful in such scenarios, but unit tests for methods that detail what the class is not capable of? They breathe the soul into your objects.