Software developers usually either love or hate writing automated tests. Fortunately, I enjoy writing tests, mostly because I have experienced their benefits.
In this post, I'll describe my personal reasons for writing tests, which took shape during the last decade, writing all kinds of tests using the majority of frameworks and tools within the JavaScript ecosystem.
Faster development
It might be counterintuitive that spending extra time writing tests could speed up development. However, without automated tests, we'll still have to "validate" somehow that the code behaves as expected, right? So, we'll have to perform manual tests.
Now, let's imagine a practical example of transforming URL strings into HTML anchor links:
<!-- Before -->
https://andreipfeiffer.dev
<!-- After -->
<a href="https://andreipfeiffer.dev">andreipfeiffer.dev</a>
Manually typing a URL into a comment box and pressing a submit button to see the result should take between 5 and 10 seconds
. However, we'll have to verify multiple URL formats, so a full suite of tests could take around 2 minutes
.
Setting up a framework for unit tests usually takes a couple of hours, but that's only a one-time effort. From personal experience, writing a complete set of tests usually takes around 20 times the effort of one manual test of the entire suite. Therefore, automated tests become faster if we perform the full suite of tests more than 20 times.
Easier debugging
We rarely write bug-free code. Therefore, we'll often have to chase these bugs in a process called debugging. The most critical part of debugging is being able to reproduce the bug. As soon as we understand when the bug occurs, it should be reasonably easy to fix.
Without any tests, we'll have to perform manual testing, try to reproduce the bug, then verify that the fix worked.
With automated tests, we have two weapons at our disposal:
- When new code is pushed to the repository, an existing test suite might catch the bug immediately in case the tests get executed in a CI.
- Even if there is no test to cover the new bug, once we understand when the bug occurs, it should be fairly easy to add a new test case to see that it fails, fix the bug, then re-run the test to make sure it passes.
Additionally, the same bug is unlikely to resurface in the future, as we already have a test case for it.
Proof of work
Writing the required code to implement a feature or fix a bug is only part of the entire development process. Unfortunately, many developers stop at this step, leaving the testing part to testers, QA engineers, their team leads, or even to the clients themselves. This is unprofessional practice.
As developers, we should put an active effort into all the steps of the development process: researching, designing, programming, documenting, maintaining, and testing.
On the other hand, manual testing rarely proves that the written code works as expected. When somebody tells me, "I've tested the feature, and it works", I have no idea what that means:
- What use cases have been tested?
- What validations were performed to conclude that it works?
On the other hand, automated tests are hard evidence that the code works as implemented. Furthermore, tests are relatively easy to read. Therefore there's nothing ambiguous about what is tested and how it's tested.
Regression prevention
Software applications evolve. Features get entangled. Changing code in some parts of the application could negatively impact existing functionalities.
Did it ever happen to you to change a single line of code, committing it, deploying the code without testing your "trivial change" and breaking the application? It happened to me countless times.
The sad truth is that developers never perform a full suite of manual regressions tests, mainly because it's not feasible:
- It would take too much time to perform all the tests manually.
- We usually hate repeating ourselves, performing the same set of manual tests over and over.
Without automated tests, it could take a long time until we even discover bugs, especially on features that are not very frequently used. But, most of the time, we don't even know all the existing features. This brings us to the next chapter.
Documentation
While developing large applications, it becomes daunting to keep track of all features, even when being involved in the development of each and every functionality. In addition, requirements can change so often that two developers could have different opinions about how a particular feature should work.
Furthermore, new team members would have a hard time understanding how a particular code should work.
Fortunately, automated tests are a great source of documentation:
- Reading the tests provides an excellent overview of the feature they assert. Even if the test names are not self-explanatory, reading the test code should give quicker insights regarding what the code does without thoroughly dissecting and understanding the whole source.
- Tests don't get outdated. If we put the effort into executing tests regularly and maintaining them like any other code, they will always reflect what's currently implemented.
Confident refactoring
Have you ever been scared of changing existing code because you didn't want to break any existing functionality? You're not the only one. It usually happens when we have no tests.
A suite of tests that assert how the code should work empowers us to safely and confidently refactor our code anytime we consider necessary. Whenever we break existing functionality, we should know about it, as the tests would fail.
Loose coupling
Writing isolated unit tests or partially integrated tests will greatly influence the way write our source code also. However, tightly coupled code is not trivial to test and often requires refactoring before making it testable.
As time goes by, we'll learn how to apply good practices and techniques for writing testable and loosely coupled code:
- Extract smaller, single-purpose functions that perform specific and explicit logic.
- Extract core logic in pure functions, as they are easier to test than stateful functions.
- Implement inversion of control to reduce dependencies to the bare minimum, etc.
The above techniques will not only aid us when writing tests. Instead, it will make us better developers overall, forever changing how we write code, but also how we generally look at code.
In case you're wondering why developers choose to avoid automated testing, here's a breakdown of the most popular excuses not to write tests.