Unit testing is one of the best ways to improve the quality and maintainability of your codebase. This article aims to articulate a few of these benefits that unit testing will provide you during and after development. This is by no means an exhaustive list, but just a few of the more substantial impacts that you will see from a robust set of unit tests.
Runtime errors vs Compile-time errors
In general, with a statically typed language, you get much more robust error checking upfront as you write and compile your code than you do with dynamically typed languages. Things like occasionally passing the wrong parameter type, forgetting to return a value, accessing an unset variable, or forgetting to import the correct library are often protected by the compiler.
All this to say, for many of the languages that do not have as robust of error checking built into the compiler, writing even the simplest of unit tests will help to exercise portions of your code that may be missing imports or passing incorrect types. If you are using one of these dynamically typed languages, then this is likely one of the largest benefits you will see when first adding unit tests.
Test-driven development (TDD)
Test-driven development is a popular development process in which you first write your tests and then implement code that helps those tests to pass. Although many people do not always follow this practice, it can be especially helpful when writing certain types of code. One situation where it can be extremely helpful is when the code is inherently complex.
If it is difficult to write your code in a clear, concise manner and requires many inline comments, then this is probably a situation where TDD would help actually speed up your development process. Just a few examples of this are things like:
- Parsing text input and serializing output.
- Aggregating data.
- State machines.
The increased speed you get with using test-driven development for these sorts of problems comes from the fact that problems like this are often easier to define how you want the feature to behave than it is to implement it in a clean manner. Think of the example given above of parsing text input. It is fairly easy to come up with a set of valid and invalid inputs and use them as test cases. However parsers are often not very readable chunks of code due to many hard-coded values and regular expressions. So in this case, writing out all of your expectations up front and making sure your code behaves correctly in those situations will likely help you implement the parser faster and with much greater confidence than manual testing.
Once you get used to the process of TDD, you may find many more cases where it actually speeds up your development process and will actually often improve the implementation of your code as well.
Testing error cases
There are often many error cases in software that are either unhandled or handled but have a bug in the handling logic. This is usually because some of these errors can often be very difficult to test manually. For example, a network request failing due to bad connectivity or 500 status code from the service you are calling. Or a caller of the API passing a number instead of a string. These are places where unit tests can shine. You can make sure that you test all of these error conditions and that they are correctly handled.
This is an example of a place where the benefit of adding unit tests is actually not something that can be achieved by manual testing. There are many notorious examples of this, including the goto fail vulnerability that was caused by a bug in error handling in Apple’s SSL code. If a good set of unit tests were written surrounding the error cases, this bug could have likely been prevented. This is just one high profile example, but many of the security vulnerabilities discovered in a common application today fall into this category.
Protecting your code in the future
Most of the benefits listed above could apply when you are developing and launching code for the first time. However, the real power of unit tests is gained over time as the tests continue running regularly. This is especially important because most codebases are continually growing and being modified by other engineers. Your tests help ensure that if someone is editing your code in the future that they won’t accidentally break past functionality.
With more and more teams moving away from longer Waterfall-style software development lifecycles and towards Agile or shorter release cycles, these tests will prove to be infinitely helpful. If you are deploying code every month, week, or day it starts to become impossible to re-test every flow manually before you release your new code. However, with proper unit test coverage and ideally some other higher-level function tests hooked up to a CI/CD pipeline, you can still maintain quality while shipping code on a daily basis.