150
6 Computing Integrals and Testing Code
6.6 Testing Code
6.6.1 Problems with Brief Testing Procedures
Previously in this book, our programs have been tested in very simple ways, usually
by comparing to hand calculations. For numerical integration, in particular, testing
has so far employed two strategies. When the exact solution was available, we
computed the error and saw that an increase of n gave a decrease in the error. When
the exact solution was not available, we could (as in the comparison example of the
previous section) look at the integral values and see that they stabilized as n grew.
Unfortunately, these are very weak test procedures and not at all satisfactory for
claiming that the software we have produced is correctly implemented.
A Deliberate Bug To see this, we can introduce a bug in the application function that calls trapezoidal: instead of integrating 3t 2 e t 3 , we write “accidentally”
3t 3 e t 3 , but keep the same anti-derivative x(t) = e t 3 for computing the error. With
the bug and n = 4, the error is 0.1, but without the bug the error is 0.2! It is of course
completely impossible to tell if 0.1 is the right value of the error. Fortunately, in this
case, increasing n shows that the error stays about 0.3 in the program with the bug,
so the test procedure with increasing n (and checking that the error then decreases)
points to a problem in the code.
Another Deliberate Bug Let us look at another bug, this time in the mathematical
algorithm: instead of computing
1
2 (f (a) + f (b)) as we should, we “forget” the
second
1
2 and write 0.5*f(a) + f(b). The error for n = 440, 400 when computing
1.9
1.1 3t 2 e t 3 dt goes like 1400, 107, 10, respectively, which looks promising.
The problem is that the right errors should be 369, 4.08, and 0.04. That is, the
error should be reduced faster in the correct than in the buggy code. The problem,
however, is that it is reduced in both codes, and we may stop further testing and
believe everything is correctly implemented.
Unit Testing
A good habit is to test small pieces of a larger code individually, one at a time.
This is known as unit testing: A (small) unit of the code is identified, so that a
separate test for this unit can be made. The unit test should be “stand-alone” in
the sense that it can be run without the outcome of other tests. Typically, one
algorithm in scientific programs is considered a unit. The challenge with unit
tests in numerical computing, is to deal with numerical approximation errors.
A fortunate side effect of unit testing is that the programmer is forced to use
functions to modularize the code into smaller, logical pieces.
6 Computing Integrals and Testing Code
6.6 Testing Code
6.6.1 Problems with Brief Testing Procedures
Previously in this book, our programs have been tested in very simple ways, usually
by comparing to hand calculations. For numerical integration, in particular, testing
has so far employed two strategies. When the exact solution was available, we
computed the error and saw that an increase of n gave a decrease in the error. When
the exact solution was not available, we could (as in the comparison example of the
previous section) look at the integral values and see that they stabilized as n grew.
Unfortunately, these are very weak test procedures and not at all satisfactory for
claiming that the software we have produced is correctly implemented.
A Deliberate Bug To see this, we can introduce a bug in the application function that calls trapezoidal: instead of integrating 3t 2 e t 3 , we write “accidentally”
3t 3 e t 3 , but keep the same anti-derivative x(t) = e t 3 for computing the error. With
the bug and n = 4, the error is 0.1, but without the bug the error is 0.2! It is of course
completely impossible to tell if 0.1 is the right value of the error. Fortunately, in this
case, increasing n shows that the error stays about 0.3 in the program with the bug,
so the test procedure with increasing n (and checking that the error then decreases)
points to a problem in the code.
Another Deliberate Bug Let us look at another bug, this time in the mathematical
algorithm: instead of computing
1
2 (f (a) + f (b)) as we should, we “forget” the
second
1
2 and write 0.5*f(a) + f(b). The error for n = 440, 400 when computing
1.9
1.1 3t 2 e t 3 dt goes like 1400, 107, 10, respectively, which looks promising.
The problem is that the right errors should be 369, 4.08, and 0.04. That is, the
error should be reduced faster in the correct than in the buggy code. The problem,
however, is that it is reduced in both codes, and we may stop further testing and
believe everything is correctly implemented.
Unit Testing
A good habit is to test small pieces of a larger code individually, one at a time.
This is known as unit testing: A (small) unit of the code is identified, so that a
separate test for this unit can be made. The unit test should be “stand-alone” in
the sense that it can be run without the outcome of other tests. Typically, one
algorithm in scientific programs is considered a unit. The challenge with unit
tests in numerical computing, is to deal with numerical approximation errors.
A fortunate side effect of unit testing is that the programmer is forced to use
functions to modularize the code into smaller, logical pieces.
