No matter how good you are, you will make mistakes. Probably more mistakes than you will ever know.
Accordingly, it is necessary to not only do what you can to not make mistakes, but also to rigorously test your code to confirm that you were successful.
You will still have bugs, though…
Our first line of defense will be unit tests: small, directed tests aimed to check whether or not the smallest units of our code are working correctly.
Ideally, our development process would be driven by our unit tests.
This concept is referred to as Test Driven Development (TDD).
Alternatively, we can restate these laws as the following list of steps:
If we do this well, then we know that all of the desired functionality has been tested, because, otherwise, it would not exist.
Also, we now have a safety net to protect us from regressions in the code base. If someone breaks our code, we should immediately know because of failing tests.
Unfortunately, it doesn’t always go so smoothly. For instance, we have to write our tests correctly, we have to test for unintended results as well as intended results (which is more difficult to do!), and, in some cases, we can’t write unit tests!
In many fields, safety relies on the “Swiss Cheese” model:
There will always be holes in any layer of safety that you implement, but if you have enough layers in places, it greatly decreases the odds that a hole will exist in the entire system.
So, we try to use our brains to make sure we right good code, and we try to write good tests. We will fail at both, but will hopefully come closer to eliminating bugs than if we didn’t test at all.
Another simple layer to add, which a lot of people overlook, is using the type system to model your problems.
Strong Static Typing can often give you similar benefits as unit testing.
In languages like Agda, Idris, Coq, you can model problems so completely that compilation “guarantees” correctness (sort of…), because invalid states can not be compiled.
In these languages, you are basically writing proofs.
This gives us:
To combat bugs in our code
So, our unit tests will allow us to ensure code performs as expected, to modify code without causing regressions, and empowers us to refactor our and improve our code without fear.
But how do we write unit tests…
Obviously. The point, though, is that you should use all of the principles you apply to writing good code in general when writing unit tests. You will need to maintain, fix, and improve tests over time just like all other code!
If you don’t follow best practices, updating tests will become such a nightmare that you will simply start deleting tests or just stop testing at all. Then regressions creep in and the game is over.
First, we need to identify what a given unit should do.
This sounds simple, but it isn’t!
We can easily create tests for the common, obvious cases, but we must also test for the boundary cases.
For instance, if we are implementing a function to calculate the area of a rectangle given its height and width, what inputs should we test?
First, we would do something obvious like area(10,5)=50.
But what happens if we are given negative values? What if we are given zeros?
We often forget about these types of inputs when writing code and just assume we will “obvious” inputs.
In this particular case, the logic is simple enough that we can easily address the problem with a couple of tests and some branching in our code.
In general, though, we really need to focus on our domains.
In general, for an integer parameter, we should probably consider the following values:
If we know we are using modular arithmetic, then we may want to test one representative from each congruence class as well.
If we are working with mod m, then maybe a good set of tests would be
-m-1, -m, …, -2, -1, 0, 1, 2, …, m, m+1
Basically, what you want to do is think about the input types, identify boundary values that are likely to cause different behavior, then test those boundary values, near those boundary values, and far away from the boundaries in all directions.
And don’t forget null values!
So what would good inputs be for a method with a List input?
What about a method that takes a Point as input?
What about String inputs?
Once we have decided what we need to test, we will write a single unit test for each interesting input and make an assertion about what its output should be.
Ideally, you should have one assertion per test. if not, all assertions should at least be testing one aspect of your code.
How can we test code that requires a database lookup?
As we stated previously, we want to isolate the code being tested from the rest of the code base.
A database lookup, though, will require database libraries, data model code, a running database, proper setup of the data in the database, etc.
The first step should be to isolate the database code from your code. You can do this by providing some interface that matches your data model and provides convenience methods for lookups.
All SQL, driver configuration, etc. will be hidden behind this interface, so your code that requires a lookup may simply call something like:
final User u = db.lookupUserById(10);
The db object can then be passed into our object or method and, since we have “programmed to an interface,” we can replace this object with any instance of the same interface.
To properly isolate the our code from the database lookup when testing, we could provide a mock for the database lookup object.
In other words, we can create a class that matches the necessary interface, but does not actually interact with a database!
In our unit test, we will create a mock that is configured to return a specific instance of the User class when the lookup method is called with the value 10, and then we can write our test with the assumption that the lookup succeeds!
If we mock all dependencies in a method or class using this, approach, then we know that any errors are caused solely by the method or class logic itself, and not by dependencies.
This also makes it easier to write tests, because a lot of code my be required to create real instances that you could use, but mocks are usually rather trivial to create.