This article explains how to engineer your test cases, what strategies to adopt to ensure your code is properly covered and how to manage your execution environment.
Choosing a testing framework is just the first part of the story. The second and most important one is to actually
fully and properly test your code. The typical code unit that you'll test is a method. The first thing that
you'll have to test is that your method works properly. For example, let's consider the following method:
double divide(double number, double divider) throws DivisionByZeroException. Your test would
look like this:
public void testMethod() {
double precision = .001;
try {
double result = divide(6, 3);
if (2 - precision < result < 2 + precision)
SUCCESS();
else
FAIL();
} catch (DivisionByZeroException e) {
FAIL();
}
}
This will tell you if your method works properly or not: first, you make sure that no exception is thrown when your method is given a valid set of parameters. You also make sure that the result returned by the method is within specified precision requirements. This is usually called "testing for success".
But what about that exception that is being thrown? It's there to enable programmers to gracefully recover
from abnormal execution conditions, such as when the divider is zero. This is a very important aspect because
software may
cause serious damage if abnormal execution conditions are not properly reported and handled. So just testing that
your testing method works properly is not enought. You should also make sure that DivisionByZeroException is thrown
when the method is invoked with a divider parameter equal to zero. This is usually called "failure
test"
and a typical failure test may look like this:
public void testMethod() {
try {
divide(6, 0);
FAIL(); // because the exception should have been thrown
} catch (DivisionByZeroException e) {
SUCCESS(); // because the exception was thrown as excepted, showing that the abnormal condition (an invalid
set of parameters) has been properly detected and reported.
}
}
The previous example was a very simple one, because the code unit that was tested is very simple. If your algorithm is more complex - that is, if its execution graph is complex - then only one success test may not be enough. To make sure your code is properly tested, you can adopt the following strategy:
if / else and switch / case constructs - this is called building
your algorithm's decision graph. A path through this graph is triggered by a set of input parameters and /
or
a particular state of the execution environment.
A code fragment is said to be stateful if the outcome of its next execution is being influenced by the outcome of previous executions. This is generally the case when data items - persistent or not - are being used in successive iterations of the code fragment.
If the behavior of a code fragment depends on the state of its execution environment, then this state must be recreated each time before the test is being executed. To achieve this it is common practice to use a "fixtures". Fixtures are code fragments which fulfill two tasks:
This may come as a surprise to you, but whoever you are, your code is buggy. If you're not aware existing bugs, that's because nobody has found them yet. When someone will find a bug in your code, however, the first thing you're supposed to do is to write a test that reproduces the bug. Your new test must fail when you feed your code the right set of parameters and the right execution environment. Then tune your code until the test passes. You should also run your other tests each time to make a modifications. This ensures that your fix to the new bug doesn't break any other functionality. This good practice and many other are well explained by the Test Driver Development methodology. I personally don't think that applying TDD 100% is best thing you could do, but their way of going about fixing bugs is a very good thing.