Lets assume you are tasked with testing a method with the following signature:
public int foo(int a, int b);
If you know nothing else about the method, what would you use for test values? Here are the values I would choose:
0, 1, -1, 4711, -2342, Integer.MIN_VALUE, Integer.MAX_VALUE
Why? Well experience shows that these values cover cases where software is likely to fail.
Why? Because at or between these values many algorithms and formulars behave different. Take the Heaviside-function as an example. It looks like this:
If you got the implementation right for 4711 it is probably correct for 4712 and 4713 as well, but bets for 0 and -1 are completely off.
So if you reverse that thought, what does this tell you about how to design or implement your software?
I think it results in two rules:
Avoid superfluous corner cases
Sometimes the way you choose to implement affects the existence of boundaries. Take for example the Standard Code Retreat programming problem of Conways Game Of Life. Follow the link if you don't know it, I'll wait.
Welcome back. When people try to implement it for the first time, about half of them use an array (or list of lists) for representing each cell. This forces them to initialize the array. Since they don't want to waste huge amounts of memory, they typically initialize only a limited area, let's say 100x100 cells.
Now we got a corner case that didn't exist in the original problem: What ever we do we have to take extra care if we do it on or next to the edge of the initialized grid.
You are probably thinking: "But my problems aren't abstract thingies on infinite grids, so this doesn't apply." Sorry, you are most probably wrong. Do you loop over collections in your code? If you do you have a new boundary condition: an empty collection vs. a non-empty collection. These two go through two significant different code paths in code structured like this:
It gets even worse when you don't use the foreach loop in the language of your choice, but iterate over an index:
It introduces the following new boundaries: 1 vs many elements (maybe you used 0 instead of i as an index). The first vs the last vs an element in the middle.
Compare this mess with something like
It doesn't have any of the boundaries.
Another example: Have you ever returned a mutable object from a method to which the owning object still had a reference? Voila, you just created another corner case: Read-Only access from the client vs. mutating access of the client.
If you have mutable state accessed by multiple threads you have probably an almost infinite number of corener cases: Every ordering on byte code level (at least) of accesses from the different threads. That's basically why concurrency is so hard.
On a more abstract level, some problems look like this:
and you can choose your solution to look like this:
or like this:
Which on is easier to check for completeness? Which is probable to need more code?
Make the necessary boundaries easy to test
Often you don't have a choice: there is a hard boundary in the requirements. Like "if the value of the transaction is larger then 1000Euro, display a warning." or "If there is unsaved data, display a * behind the title."
In these cases make sure the corresponding decision is made exactly once in your code. That piece of code should probably do nothing but that distinction. Make sure you unit test it well.