One year ago when I first started as a software engineer I would have been surprised to see Future Scott writing a blog post about this topic. Tests seemed like something that only added a layer of red tape to pushing code into production.
I was first introduced to unit testing back in college in what seemed like a really silly assignment. We had a class with some fairly simple methods on it, then we wrote tests to verify that the inputs produced the correct outputs. The problem with the assignment were that:
- After we turned in the assignment, the code was never seen or run again
- The non-test code was entirely too simple and not anything that was generic enough to enable us to understand the merits of the test (not to say that tests weren’t warranted, but simply that we couldn’t see the merit)
- No Context of WHY unit tests are written was conveyed. The assignment left me thinking that professional engineers were generally idiots if they needed to validate that a function that added two numbers worked correctly if indeed my professor was telling the truth.
Since then, I’ve become an advocate of tests, even though I generally dislike writing them. Here’s the path that led me to that conclusion.
I’ve broken production code
Not only did I break production code, but I made a really simple mistake that had huge detriment. Imagine if a construction worker accidentally flipped the signs for North and South on a freeway, and that’s about the worst breakage I’ve created. Except also a bunch of cars crashed and a few of the cars caught fire, and then fog from the bay swept in, and then other cars couldn’t see the wreckage and they crashed into the stationary cars on fire, and then some of the people that were in the burning cars ended up turning into zombies and then started a zombie outbreak (metaphorically). And then to continue with the metaphor, the other Hearsay Engineers swooped in on helicopters with 30mm cannons on them and had to shoot all the zombies that had popped up. The customer support team was metaphorically rushing in with machetes as well hacking away at what used to be other human beings. John “Stonewall” Williams, the head of customer support, emerged a hero that day. Meanwhile I just stood there wondering what I should be doing at that point. So, I was told to go write regression tests to ensure that no one else accidentally flipped the North and South signs on the freeway again.
Had I taken the time to simply write solid test coverage before merging the code into production, there would have been no incident. This was my first true realization that tests were in fact valuable.
I wrote some tests as a formality to check the box, but then…
There were in fact errors. Even though I was 100% confident in my code, as I wrote tests to cover all possible (or at least most possible) cases, I’d uncovered some of my blunders by running my code with different inputs. Any developer, regardless of skill or expertise, is still human, and errors are therefore inevitable over time.
You can uncover unaccounted for cases
Along with the above case in which blunders are uncovered, it sometimes help to deliberately go over every function written and think through the possible cases that can be produced. During that process, seemingly obvious unaccounted for cases sometimes surface. This happened to me just today as I was writing test cases for indexing objects into Haystack, and I suddenly realized that I’d obviously accounted for saving and updating, but hadn’t even given any thought to deletes. As I started to write some code to account for deletes, Ryan “Flying Fist” Newton, who was sitting next to me and working on a similar project, asked if I’d accounted for deletes as he coincidentally made the same realization with his work.
You can really up your Github stats!
Sure, no one actually cares about the number of lines of code written. Except me.
Enforces good coding habits
Better and cleaner code is generally easier to test. Therefore, if you are writing tests in conjunction with your code, you are then forced to maintain some good habits. This means that functions are concise with clear inputs and outputs. It means that mutations inside of a method are kept to a minimum.
Code can be maintained with confidence
I was introduced to Uncle Bob Martin from the Giant Robot Smashing Into Other Giant Robots Podcast. He’s a huge advocate of test driven development, meaning that a failing test is created before production code is even written. I haven’t made the jump that far, but it’s certainly a goal to work toward. A quote from a post that he wrote:
“Confidence. A well designed test suite with a high degree of coverage eliminates, or at least strongly mitigates the fear of change. And when you aren’t afraid to change your code, you will clean it. And if you clean it, it won’t rot. And if it doesn’t rot, then the software team can go fast.”
This can be restated in a number of ways. The particular post I referenced asks the rhetorical question of how often you as a developer end up getting slowed down by bad code.
Personally, I’ve found instances of code that I wanted to refactor, but because the code was so messy and because there wasn’t surrounding test coverage, I had to instead leave it in place because that was the safer thing to do as though the block of code was a giant jenga tower that would collapse at the slightest touch.
Code can be merged faster
Contrary to my initial perception of tests being a layer of red tape, I’ve found that writing corresponding tests to my code ends up getting it merged into production way faster. I’ve only been with one software company, so I’m not necessarily familiar with practices elsewhere, but all code that I write has to be peer reviewed before it gets merged into production. I like writing code, and I like to write a lot of it really fast. But I’m only as fast as how willing my peers are to review my work. If there are no tests or insufficient tests, then I’m putting other people on the hook to ensure that my code is good to go and error-free, and the process for getting things reviewed takes way longer. I’ve found that taking the time to sit down and write tests, however boring they may be, increases the churn in getting my work into production.
Other people won’t break your code
To reference my initial story about the unit tests we wrote in college that no one understood, the point of tests is not necessarily to check your own work, but to ensure that other people behind you who may not fully understand everything that you did or everything that code is supposed to do, especially in a dynamically typed language like Python as we use. Tests therefore lose much of their purpose if the code being written is meant for one time use, but real-world programming involves large projects that are maintained for long periods of time and refactored constantly.