Structured testing uses cyclomatic complexity and the mathematical analysis of control flow graphs to guide the testing process. Structured testing is more theoretically rigorous and more effective at detecting errors in practice than other common test coverage criteria such as statement and branch coverage [WATSON5]. Structured testing is therefore suitable when reliability is an important consideration for software. It is not intended as a substitute for requirements-based "black box" testing techniques, but as a supplement to them. Structured testing forms the "white box," or code-based, part of a comprehensive testing program, which when quality is critical will also include requirements-based testing, testing in a simulated production environment, and possibly other techniques such as statistical random testing. Other "white box" techniques may also be used, depending on specific requirements for the software being tested. Structured testing as presented in this section applies to individual software modules, where the most rigorous code-based "unit testing" is typically performed. The integration level Structured testing technique is described in section 7.
5.1 The structured testing criterion
After the mathematical preliminaries of section 2 (especially Sec. 2.3), the
structured testing criterion is simply stated: Test a basis set of paths
through the control flow graph of each module. This means that any
additional path through the module's control flow graph can be expressed as
a linear combination of paths that have been tested.1. A test set of v(G) paths can be realized. (Again, see section
9.1 for discussion of the more general case in which actual complexity is
substituted for v(G).)
2. Testing beyond v(G) independent paths is redundantly
exercising linear combinations of basis paths.
Several studies have shown that the distribution of run time over the
statements in the program has a peculiar shape. Typically, 50% of the run
time within a program is concentrated within only 4% of the code [KNUTH]. When the test data is derived
from only a requirements point of view and is not sensitive to the internal
structure of the program, it likewise will spend most of the run time
testing a few statements over and over again. The testing criterion in
this document establishes a level of testing that is inherently related to
the internal complexity of a program's logic. One of the effects of this
is to distribute the test data over a larger number of independent paths,
which can provide more effective testing with fewer tests. For very simple
programs (complexity less than 5), other testing techniques seem likely to
exercise a basis set of paths. However, for more realistic complexity
levels, other techniques are not likely to exercise a basis set of paths.
Explicitly satisfying the structured testing criterion will then yield a
more rigorous set of test data.
5.2 Intuition behind structured testing
The solid mathematical foundation of structured testing has many advantages
[WATSON2]. First of all, since any
basis set of paths covers all edges and nodes of the control flow graph,
satisfying the structured testing criterion automatically satisfies the
weaker branch and statement testing criteria. Technically, structured
testing subsumes branch and statement coverage testing. This means that
any benefit in software reliability gained by statement and branch coverage
testing is automatically shared by structured testing.
Next, with structured testing, testing is proportional to complexity.
Specifically, the minimum number of tests required to satisfy the
structured testing criterion is exactly the cyclomatic complexity. Given
the correlation between complexity and errors, it makes sense to
concentrate testing effort on the most complex and therefore error-prone
software. Structured testing makes this notion mathematically precise.
Statement and branch coverage testing do not even come close to sharing
this property. All statements and branches of an arbitrarily complex
module can be covered with just one test, even though another module with
the same complexity may require thousands of tests using the same
criterion. For example, a loop enclosing arbitrarily complex code can just
be iterated as many times as necessary for coverage, whereas complex code
with no loops may require separate tests for each decision outcome. With
structured testing, any path, no matter how much of the module it covers,
can contribute at most one element to the required basis set.
Additionally, since the minimum required number of tests is known in
advance, structured testing supports objective planning and monitoring of
the testing process to a greater extent than other testing strategies.
void func()
{
if (condition1)
a = a + 1;
if (condition2)
a = a - 1;
}
Figure 5-1. Example C function.
5.3 Complexity and reliability
Several of the studies discussed in Appendix A show a correlation between
complexity and errors, as well as a connection between complexity and
difficulty to understand software. Reliability is a combination of testing
and understanding [MCCABE4]. In
theory, either perfect testing (verify program behavior for every possible
sequence of input) or perfect understanding (build a completely accurate
mental model of the program so that any errors would be obvious) are
sufficient by themselves to ensure reliability. Given that a piece of
software has no known errors, its perceived reliability depends both on how
well it has been tested and how well it is understood. In effect, the
subjective reliability of software is expressed in statements such as "I
understand this program well enough to know that the tests I have executed
are adequate to provide my desired level of confidence in the software."
Since complexity makes software both harder to test and harder to
understand, complexity is intimately tied to reliability. From one
perspective, complexity measures the effort necessary to attain a given
level of reliability. Given a fixed level of effort, a typical case in the
real world of budgets and schedules, complexity measures reliability
itself.
5.4 Structured testing example
As an example of structured testing, consider the C module "count" in Figure
5-2. Given a string, it is intended to return the total number of
occurrences of the letter `C' if the string begins with the letter `A'.
Otherwise, it is supposed to return -1.
|
Input
|
Output
|
Correctness
|
|---|---|---|
|
X
|
-1
|
Correct
|
|
ABCX
|
1
|
Correct
|
Figure 5-4. Tests for "count" that satisfy statement and branch coverage
|
Input
|
Output
|
Correctness
|
|---|---|---|
|
X
|
-1
|
Correct
|
|
ABCX
|
1
|
Correct
|
|
A
|
0
|
Correct
|
|
AB
|
1
|
Incorrect
|
|
AC
|
0
|
Incorrect
|
Figure 5-5. Tests for "count" that satisfy the structured testing criterion.
The set of tests in Figure 5-5 detects the error (twice). Input "AB" should
produce output "0" but instead produces output "1", and input "AC" should
produce output "1" but instead produces output "0". In fact, any set of
tests that satisfies the structured testing criterion is guaranteed to
detect the error. To see this, note that to test the decisions at nodes 3
and 7 independently requires at least one input with a different number of
`B's than `C's.
5.5 Weak structured testing
Weak structured testing is, as it appears, a weak variant of structured
testing. It can be satisfied by exercising at least v(G) different paths
through the control flow graph while simultaneously covering all branches,
however the requirement that the paths form a basis is dropped. Structured
testing subsumes weak structured testing, but not the reverse. Weak
structured testing is much easier to perform manually than structured
testing, because there is no need to do linear algebra on path vectors.
Thus, weak structured testing was a way to get some of the benefits of
structured testing at a significantly lesser cost before automated support
for structured testing was available, and is still worth considering for
programming languages with no automated structured testing support. In
some older literature, no distinction is made between the two criteria.5.6 Advantages of automation
Although structured testing can be applied manually (see section 6 for a way
to generate a basis set of paths without doing linear algebra), use of an
automated tool provides several compelling advantages. The relevant
features of an automated tool are the ability to instrument software to
track the paths being executed during testing, the ability to report the
number of independent paths that have been tested, and the ability to
calculate a minimal set of test paths that would complete testing after any
given set of tests have been run. For complex software, the ability to
report dependencies between decision outcomes directly is also helpful, as
discussed in section 9.5.7 Critical software
Different types of software require different levels of testing rigor. All
code worth developing is worth having basic functional and structural
testing, for example exercising all of the major requirements and all of
the code. In general, most commercial and government software should be
tested more stringently. Each requirement in a detailed functional
specification should be tested, the software should be tested in a
simulated production environment (and is typically beta tested in a real
one), at least all statements and branches should be tested for each
module, and structured testing should be used for key or high-risk modules.
Where quality is a major objective, structured testing should be applied
to all modules. These testing methods form a continuum of functional and
structural coverage, from the basic to the intensive. For truly critical
software, however, a qualitatively different approach is needed.
Although the specialized techniques for critical software span the entire
software lifecycle, this section is focused on adapting structured testing
to this area. Although automated tools may be used to verify basis path
coverage during testing, the techniques of leveraging functional testing to
structured testing are not appropriate. The key modification is to not
merely exercise a basis set of paths, but to attempt as much as possible to
establish correctness of the software along each path of that basis set.
First of all, a basis set of paths facilitates structural code walkthroughs
and reviews. It is much easier to find errors in straight-line computation
than in a web of complex logic, and each of the basis paths represents a
potential single sequence of computational logic, which taken together
represent the full structure of the original logic. The next step is to
develop and execute several input data sets for each of the paths in the
basis. As with the walkthroughs, these tests attempt to establish
correctness of the software along each basis path. It is important to give
special attention to testing the boundary values of all data, particularly
decision predicates, along that path, as well as testing the contribution
of that path to implementing each functional requirement.