“No amount of testing can prove a software right, but a single test can prove a software wrong.”— Amir Ghahrai
Many developers think that Unit Testing is like flossing. Everybody knows it’s good, but many don’t do it. Some even think that with good code, tests are unnecessary. For a small project you are working on, that might be ok. You know the definition, the implementation details, and the desired behavior because you created them. For any other scenario, it’s a slippery slope, to say the least.
Keep on reading and learn why unit tests are an essential part of your development workflow and how you can start writing them for your new and legacy projects. For those who are unfamiliar to unit testing, you might want to start with a thoughtful article about what they are.
An unexpected morning regression
Last week I came into the office, grabbed my first coffee, leaned on my chair and start sipping from my old Tarantino’s cup while reading a bunch of emails, as usual. Eventually, I opened the code and faced what I had left undone the day before. “What was I thinking about?” I muttered as I started pounding the keyboard. “I managed to fix that!”
A few days later, we discovered a regression caused by that same line of code. Shame on me. “How could our unit tests allow this to happen?” Oops! No tests whatsoever, of any kind. Now, who’s to blame? Nobody in particular, of course. All of us are responsible for the codebase, even for the code we didn’t write, so it’s everybody’s fault. We need to prevent this from happening again. I usually forget what I broke — and, especially what I fix— these missing tests should be the first to start with.
Here are a few steps I should have followed before crashing our codebase first thing in the morning:
- If I change any code, I am changing its definition and the expected behavior for any other parts involved. Unit tests are the definition. “What does this code do?” “Here are the tests, read them yourself.”
- If I create new code, I am assuming it works not just for my current implementation, but for others to come. By testing, I force myself to make it extendable and parameterizable, allowing me to think about any possible input and output. If I have tests that cover my particular case, it is easy to cover the next ones. By testing, we realize how difficult it could be for others to extend our first implementation. This way, our teammates won’t need to alter its behavior: they will inject it!
- If I write complex code, I ally encounter someone that puts me to the test: “Does this work?”, “Yes, here are the tests. It works”. Tests are proof that it works, your best friend and lawyer. Moreover, if someone messes up and your code is included somewhere, chances are developers summon you to illuminate the situation. Probably your tests will guide you to narrow the issue.
- If I am making a new feature, I should code the bare minimum necessary for it to work. Writing tests first, before actually writing any real code is the fastest and most reliable way to accomplish that. I can estimate how much time I spend writing tests. I cannot estimate how long I will spend in front of the debugger trying to figure out where things went south because I made the whole thing a little too complicated.
Now I want to write unit tests. What’s next?
Let’s say I have convinced you that tests are not a dispensable part of our daily work, but your team does not believe in this. Maybe they think there is still too much work to do, of that if you were to write all the missing tests, that would take weeks, even months! How can you explain this to your Product Owner?
Here’s the thing: you won’t. If testing becomes a time-demanding task that requires it to be on a plan or a roadmap, it won’t likely ever take off. However, I want to offer you some tips to get started with testing that would work both if you have a significant deficit in test or you just started a new project:
- Write unit tests first if you don’t know where to start.
- Only write tests for the code you made and understand.
- Don’t test everything. Just ensure that you add a couple of tests will every time you merge code.
- Test one thing only. I’d rather maintain five simple tests than one complex one.
- Test one level of abstraction. This means that when you test a component which affects others, you can ignore them. Make the component testable instead of testing everything around it.
- If some new code is too complex to test, don’t. Rather, try to split it into smaller pieces and test each individually.
- Don’t assume current locales or configuration. Run tests using different languages and time zones, for instance.
- Keep them simple: Arrange just one “System Under Test” (SUT), perform some action on it to retrieve an output, and assert the result is the one you want.
- Don’t import too much stuff into test suites. The fewer components involved, the easier it is to test yours.
- Start testing the borders of the system, the tools, and utility libraries. Create compelling public APIs and test them. Ignore implementation details, as they are likely to change, and focus on input and outputs.
Remember, these tips work well for a codebase with no tests. The very first time you are about to fix, refactor or change the behavior of any part of the code, you must write the tests first to ensure you are not breaking anything. However, when working with legacy code, you would likely see the test coverage increase as the code changes.
In this blog post, we included some pieces of advice taken from our own experience with unit testing. There are other types of tests, but if you and your team want to start testing, unit tests suit you best.
Unit tests are more “straight to the point” than any other kind since they focus on validating single parts of a more complex codebase. If you are new to them, don’t panic: start from the smallest piece and build upon that. You’ll learn a lot along the process, and detect implicit dependencies or troublesome APIs you had previously skipped.
One nice thing about testing is that you make a massive leap towards coding from the outside out — instead of from the inside out, which is usually better for the implementer, and never for the user — which turns out to create a more elegant, comprehensive, and extendable code. It goes without saying that manual testing is still a thing.
What’s your experience with testing? Is there any other tip you would suggest to newcomers? Drop us some lines in the comments or ping me directly in Twitter @Maquert.
Photos by Markus Spiske and Isis França on Unsplash.