Upload
priyaspv
View
217
Download
0
Embed Size (px)
Citation preview
7/28/2019 C++ testing
1/13
IPL Information Processing Ltd C++: Its Testing, Jim, Page 1 of 13
November 1997 But Not As We Know It
This paper was presented at EuroSTAR97 in Edinburgh 24-28 November
1997. The full reference to it is: Dorman, M.N., C++ - "It's Testing, Jim,But Not As We Know It", Proceedings of the Fifth European Conferencein Software Testing, Analysis and Review, Edinburgh, Scotland,
November 1997. (see http://www.sqe.com/euro/eurhome.html for further
details).
C++"It's Testing, Jim,
But Not As We Know It"Misha Dorman
IPL Information Processing Limited
Eveleigh House
Grove Street
BATH BA1 5LR
[email protected] http://www.iplbath.com
Abstract
Object-oriented technologies, and the C++ language in particular, have enjoyed great popularity in
recent years. Proponents have promised many benefits, including reduced lifecycle costs, increased
reliability and improved maintainability. Unfortunately, not all of these benefits have been fully
realised. One area where OO methods are relatively immature is testability and testing.
The nave application of traditional testing techniques to object-oriented C++ software has not been
completely successful. Isolation testing a technique successfully used to divide and conquer
traditional testing problems becomes unmanageable when applied to OO software systems.
This paper explores the problems from a practical point of view, and discusses an improved
integration testing approach based on an extension to the widely used source code instrumentation
technique. This approach provides many of the advantages of isolation testing without forcing the
tester to laboriously write simulated code (stubs) for the rest of the system.
Measuring test effectiveness of OO software using traditional coverage metrics can be unreliable and
misleading. This paper discusses some metrics which provide more insight into the dynamic
complexity of the software under test. In particular, they attempt to measure the extent to which the
polymorphic features of the software have been exercised. In addition, modifications are proposed to
the traditional coverage metrics, to make them more appropriate to C++ software.
By combining the enhanced integration testing techniques and new coverage metrics described in this
paper, together with appropriate tool support, C++ testing will be more efficient and more effective.
7/28/2019 C++ testing
2/13
IPL Information Processing Ltd C++: Its Testing, Jim, Page 2 of 13
November 1997 But Not As We Know It
1. Testing As We Know It
A vast amount of experience has been gained in the testing of software systems. Most of this
experience applies to software written using procedural languages (C, Ada83), using top-
down design methods.
Typically, the focus of testing is dynamic testing the software under test is executed, and the
actual behaviour compared with the expected behaviour. A dynamic test harness is used to
drive and control the test: it sets up the input data required for the test, invokes the software
under test, and compares the resulting output data with the expected values. Dynamic testing
is performed at all levels from unit, through integration, to system-level testing.
During unit testing, isolation techniques are often used to divide and conquer large testing
problems. Through the use of stubs, individual units (typically functions) are tested in
isolation from the rest of the system. Isolation testing allows units to be tested before they are
integrated with the rest of the system1. When units are subsequently integration tested, test
effort can be concentrated on the correct operation of the interfaces between units. The use of
stubs (which form part of the test harness) also has benefits during maintenance: re-testing at
the unit level is limited to those units which have actually changed. It is not necessary to re-
test all the code which depends on the changed units.
At all levels of testing, but particularly at the unit and integration levels, structural coverage
analysis methods are used to measure the quality of the test. During unit testing, statement,
decision and condition coverage metrics are used to ensure that all parts of the code have been
exercised. During integration testing, entry-point and call-pair coverage metrics are used to
ensure that the interactions between units have been fully exercised.
2. Why Is Testing C++ Harder?
The C++ language is considered by many to be merely an extension to the C language.
Some practitioners have claimed that testing C++ is no different from testing C - simplyredefine the unit from function to class and carry on as normal. It is true that the traditional
approaches to test case design (error guessing, domain coverage, state-based testing and
others) are still useful and valid. However, attempts to apply traditional testing approaches to
C++ software have encountered significant difficulties. Some of these difficulties stem from
the use of object-oriented design methods; others from intrinsic features of the C++ language.
2.1 Too Many Dependencies
Typical object-oriented designs consist of a large number of interacting components, each
relatively small by itself. This is an important strength of OOD it provides the power to
break down large, complex problems into smaller ones.An unavoidable consequence of such designs is a massive increase in the number of
dependencies between units. These dependencies include containment, inheritance, reference,
parameters, and return values.
Attempting to use isolation techniques during unit testing of most OO systems is bound to
fail. The overhead involved in the creation of test stubs for every dependency is simply too
great.
1 even before the rest of the system is written. This allows much greater flexibility in the allocation of work; it is
not necessary to complete coding before unit testing begins.
7/28/2019 C++ testing
3/13
IPL Information Processing Ltd C++: Its Testing, Jim, Page 3 of 13
November 1997 But Not As We Know It
As if these problems were not enough, features of the C++ language create even more
dependencies. Class implementation details are exposed in header files. Although clients are
prevented from taking advantage of this, it causes significant problems during isolation
testing.
In order to stub a constructor for a class, it is necessary to provide initialisation values for all
data members of the class2
- including the private ones3. However, the stub is not part of the
class (it is part of the test for some other class), and as such should have no knowledge of, noraccess to, the implementation details.
Remember, one of the reasons for isolation testing was to reduce the re-testing effort during
maintenance. When constructors are stubbed, this benefit is lost: changes to thousands of
stubs, in the unit tests for hundreds of different classes, may be required whenever the
implementation of a class is changed. Even if the class didnt have any problematic data
members when first written, they could be added at any time during maintenance.
Clearly, this situation is unacceptable we have been forced to forego data hiding and
abstraction. With them goes one of the main benefits of OO, the ability to change the
implementation of a unit without requiring changes to its clients.
2.2 Integration Testing
We have seen above that isolation testing is inappropriate for most OOD/C++ systems. The
alternative to isolation testing is bottom-up testing. First the lowest level units are tested
(those that have no dependencies on other units, and therefore do not require isolation). Then,
the units which directly depend on the lowest level units are tested in integration with the
already tested units. In this manner, testing proceeds up the dependency hierarchy, until the
complete system has been integrated and tested.
At each level of bottom-up testing, the test harness must target both the low-level structural
faults normally associated with a unit-level isolation test, and the interaction faults normallytargeted by an integration test. This combination makes bottom-up testing more complex than
either unit or integration testing alone.
The increased size of the lump which is the software under test makes it difficult to exercise
all parts of the units code. The influence that the test harness has over the software under test
is limited to defining an execution environment and passing in parameters. As testing
proceeds to higher levels, the already tested units start to get in the way of the unit testing of
the highest level units sanitising the input environment and making it impossible to test
obscure cases or error handling code.
In typical C++ systems, these problems are exacerbated by the use of data hiding and the
presence of circular dependencies between units. Data hiding makes it even more difficult forthe test harness to force the execution of difficult cases: it is prevented from setting the
private data, or calling the private member functions, of the class under test. Circular
dependencies force units to be integrated in groups instead of individually, making effective
unit testing even harder to achieve.
2More accurately, all the data members which are of class type, and which do not have default constructors.
There is often at least one such member - and as we shall see, even one is too many.3
Of course, usually all the data members are private.
7/28/2019 C++ testing
4/13
IPL Information Processing Ltd C++: Its Testing, Jim, Page 4 of 13
November 1997 But Not As We Know It
2.3 Re-use
Much of the hype surrounding object-oriented development has focused on re-use. This
overloaded term can apply to the re-use of code, designs, or architectures; here we are
concerned primarily with the re-use of code.
Code re-use predates OOD, of course. Every time a programmer uses a library function, they
are re-using code. Such re-use is limited, however, to that which is available in libraries.
What is different about re-use in OO systems is that code is re-used in a way that providesdifferent or additional functionality, without duplicating the common functionality. This
provides the flexibility which enables large-scale re-use of components. In C++, re-use is
provided through inheritance and template instantiation.
When a class inherits a member function from its base class, it is re-used in a new context.
The behaviour of the inherited member can change, for example if it calls a virtual member
function which has been overridden in the derived class.
When a template is instantiated on a new type, it is also being re-used in a new context. The
behaviour of the template can change because its use of the operation +, for example, means
different things when applied to different types.
Re-use through inheritance is a dynamic, run-time feature. The behaviour of the inherited
member function depends on the actual (dynamic) type of the object to which it is applied.
Re-use through template instantiation, on the other hand, is a compile-time feature4. The
behaviour of the template depends on the static (compile-time) type on which it is instantiated.
In both cases, the behaviour of the re-used code is (potentially) different in each new context.
In [Harrold], criteria were defined for the re-testing of re-used software. In essence, re-testing
is necessary if the behaviour of the re-used component is changed5
in the new context.
Every time code is re-used instead of written from scratch, effort is saved. However, testing
may account for 30-50% of development effort. If a new test script is required whenever the
unit is re-used in a new context, then the savings obtained through re-use of code will be
significantly reduced.
2.4 Coverage
Typical OO systems consist of a large number of relatively small member functions.
Applying traditional coverage metrics to these systems is certainly possible, and necessary.
However, achieving 100% decision and condition coverage during a class test is usually easier
than achieving the same level of coverage on the equivalent system written in a procedural
language,
In an object-oriented system, an important part of the control flow arises from polymorphism(virtual functions in C++). In an equivalent procedural system, this control flow would
probably have been written as explicit if or switch decisions - and would be targeted by
traditional structural coverage techniques. In the OO system, this control flow is hidden from
the coverage analysis because the definitions of the coverage metrics are too restrictive.
As well as missing polymorphic control flow, traditional coverage metrics fail to take
advantage of design information made explicit in OOD which can enable more advanced
coverage analysis. In particular, many classes can be modelled as state machines. The
4In typical C++ implementations, re-use via templates results in duplication of object code. However, this is
purely a compiler issue - the important thing is that the source code is re-used, unchanged, and automatically.5
Or potentially changed.
7/28/2019 C++ testing
5/13
IPL Information Processing Ltd C++: Its Testing, Jim, Page 5 of 13
November 1997 But Not As We Know It
potential states in which an object can be, are as important a structural characteristic as the
potential branches that can be taken in a member function, and as such should be measured as
part of coverage analysis.
Clearly, new coverage metrics are required if we are to benefit from structural coverage
analysis techniques.
3. SolutionsNow that we know the problems we face - what can we do about them? The techniques
described below tackle each problem in turn.
3.1 Design For Testability
The first problem is the inappropriateness of isolation testing for C++ systems. Remember,
this is due to an increase in the number of stubs required (making isolation more expensive),
and problems with stubbing constructors (making isolation impossible without breaking
encapsulation). The increase in the number of dependencies is in many cases an unavoidable
result of OO design. However, consideration of dependencies during design can help avoid anunnecessary increase in the dependencies between components thus reducing testing effort
(see [Lakos] for more details).
To solve the isolation problem, consider what sorts of classes couldbe stubbed: classes with
no (private) data members. Abstract Base Classes (ABCs) are a perfect example of stubbable
classes.
ABCs and their partners, Concrete Implementation Classes (CICs) form a technique for
completely separating the class interface from its implementation. Normally, a C++ class
declaration defines both the interface (the public part) and some features of the
implementation (the private part) in a single place. As we have seen, this causes problems
when we attempt to stub these classes. In the ABC/CIC technique, the class is split into twoclasses:
1. the ABC which defines the (public) interface to the class (as pure virtual member
functions);
2. the CIC which inherits (publicly) from the ABC, and provides the implementation
of the class6.
Clients of the class depend only on the abstract base class, not on the implementation class.
To stub the class, we retain the ABC, but provide an alternative (stub) implementation class.
6By implementing (overriding) the pure virtual functions declared in the ABC.
7/28/2019 C++ testing
6/13
IPL Information Processing Ltd C++: Its Testing, Jim, Page 6 of 13
November 1997 But Not As We Know It
Consider the following small sub-system (arrows indicate a dependency):
SoftwareUnder Test
External Class A
External Class C
External Class D
External Class B
Attempting to isolate the software under test would involve stubbing all the external classes
which we have seen is difficult or impossible.
Now consider what happens if we use ABCs to separate the interface and implementation for
each of the external classes:
SoftwareUnder Test
External Class B(interface)
External Class B(implementation)
External Class A(interface)
External Class A(implementation)
External Class C(interface)
External Class C(implementation)
External Class D(interface)
External Class D(implementation)
On the surface, this design looks more complex after all there are more classes! But
consider how we would test the class with this design. We can stub the external classes A, Band C, and external class D can be omitted from the test altogether (it is only depended on by
the implementations of B and C, which have been stubbed):
SoftwareUnder Test
External Class B(interface)
Stub for B
External Class A(interface)
Stub for A
External Class C(interface)
Stub for C
7/28/2019 C++ testing
7/13
IPL Information Processing Ltd C++: Its Testing, Jim, Page 7 of 13
November 1997 But Not As We Know It
Note that in this example we have omitted the details of how objects are created. Clearly, the
code which creates the object will depend on the implementation class (whereas code which
only uses the object depends only on the interface class). The usual technique for managing
these dependencies is to use the Factory Method pattern.
This technique looks to be the answer to our isolation testing problems.7
Unfortunately, it is
not.
There is an overhead in the use of the virtual member functions each virtual member
function call involves an additional indirect memory access8. This overhead would be
unacceptable in many systems, if applied throughout. There is a trade-off to be made between
the goals of efficiency, testability and maintainability.9
In addition, as testers we are often not able to influence the design of the system. The testing
phase is considered by some to start after design is complete, rather than as an essential part of
the complete software development process.
In the real world, the most we can hope for is that ABCs are used to isolate significant sub-
systems. Within those subsystems, though, we shall still be restricted to bottom-up
integration testing techniques.
3.2 Instrumentation for Testing
We have resigned ourselves to using bottom-up integration testing, for at least some of our
class (unit-level) testing. Some sub-systems will be stubbed, but we will be integrating with a
number of (already tested) units.
It is difficult to perform unit-level testing during an integration test, for the reasons described
above. Since we will be concentrating much more on integration testing, we want to make it
as painless as possible.
The problems we expect during bottom-up testing are due to the data and behaviour of thesoftware under test being hidden from the test harness either by the data hiding features of
the language, or by the processing performed by the other units which are linked with the test.
The first stage towards making integration testing easier is to allow the test harness access to
the implementation details of the class under test, through a friend declaration. This allows
the test harness to:
1. verify the correct implementation of the class by examination of its private data;
2. call private (helper) functions of the class to ensure that they are fully tested;
3. initialise private data of the class to force particular execution paths.
The second stage is to allow the test harness access to the calls made from the class under testto the other linked-in units. This is achieved through call interface instrumentation, an
extension to the commonly used source code instrumentation technique used for coverage
analysis.
7It also has re-use and maintenance benefits: new implementation classes can be added to the system without
changing client code. See [Martin] for more on this.
8Virtual function calls have also been shown to reduce the effectiveness of modern super-pipelined CPUs,
further impacting performance.
9 An alternative solution is to hide implementation data behind a void* pointer_to_impl_data (rather than behind
an abstract class interface). This approach suffers from the same efficiency and overhead problems as the use of
ABCs.
7/28/2019 C++ testing
8/13
IPL Information Processing Ltd C++: Its Testing, Jim, Page 8 of 13
November 1997 But Not As We Know It
Call-back functions are called immediately before and after the original call; the call-backs
wrap the original call:
SUT
ExternalClass/
Routine
"before"
"after"
Call, passing parameters
Return, or throw exception
Verify parameters
Change return value,or throw exception
Change parameters
Wrapping gives the test harness access to:
1. the order that calls are made;
2. the parameters passed to each call;
3. the return value from the linked-in function.
In addition, the test harness is able (through the call-back mechanism) to:
1. modify the return value passed back to the software under test;
2. throw an exception instead of returning a value;
3. modify any output parameters that are passed back to the software under test;
4. modify any other parameters before they are passed to the linked-in function.
Wrapping makes it easy to force special cases, such as error conditions, in order to test all
parts of the class under test. For example, to verify that the software under test correctly
handles the possibility of an exception thrown by a 3rd
party database access library might be
impossible using normal bottom-up testing techniques how would we force the library to
throw the exception?
Using wrapping, such as test is simple to implement. The appropriate call to the library is
wrapped, and the call-back functions cause an exception to be thrown. This exception is then
propagated to the software under test, as if it came directly from the database library.
Call interface instrumentation is in many ways similar to coverage instrumentation. Bothtechniques are implemented using source code instrumentation, whereby instrumentation code
is added to the source code to provide the test harness with information about the execution of
the software under test which would normally be hidden.
Coverage instrumentation provides the test harness with information on the proportion of
functions, statements, decisions and conditions which have been exercised during the test.
The instrumentation code records the execution of each statement (or decision, condition etc.)
as it happens.
Call interface instrumentation provides the test harness with information on the exact calls
which have been made, the order in which they occurred, and the parameters passed to each
call. The instrumentation code records the calls made, and the parameters passed.
7/28/2019 C++ testing
9/13
IPL Information Processing Ltd C++: Its Testing, Jim, Page 9 of 13
November 1997 But Not As We Know It
Wrapping, in combination with a bottom-up test strategy, provides many of the benefits of
isolation testing, with increased flexibility, and without the scalability and maintenance
problems which are associated with stubbing C++ classes. The following table compares the
features of isolation testing and wrapping:
Isolation Testing Wrapping
Check call order i
(optional)
Check parameters (optional) (optional)
Call original function ii
Set return value (optional)
Throw exception (optional) (optional)
Change output parameters (optional) (optional)
Call original function with modified
parameters
Use with system calls iii
Use selectively (based on call-site, as well as
function called)
Original function is linked with test
iBecause a stub must always provide a return value, and cannot call the original function, it must always know where we are in the test in
order to create the correct return value.
ii A stub function is simply a replacement implementation for the original function. The two cannot be linked together in the same test
harness, since they have exactly the same linkage name.
iii In general, isolation testing cannot be used with system calls. Consider a test harness which stubbed the exit() function: how would it
terminate?
3.3 Re-use of test cases
Whenever we re-use a software component in a context which requires re-testing, we should
also re-use the corresponding test cases. This re-use will be done not just at the test planning
level, but through direct re-use of the test case code. Designing and implementing re-usable
test cases will require more effort to make them re-usable, in the same way that creating re-
usable components is harder than creating single-purpose components. In both cases, the pay-
off for the initial investment comes later, when the component is re-used, either elsewhere in
the system, or in other systems.
3.3.1 When to Re-use?The usual example of re-use in C++ is through the inheritance mechanism. In particular, the
use of public inheritance implies the existence of an isA relationship between the classes.
Of course, the compiler cannot enforce the rule that public inheritance is used only when there
is an isA relationship the compiler has no knowledge of the semantics of the classes.
However, using public inheritance when there is no such relationship is asking for trouble.
Any function which is declared with a Base& (or const Base&) parameter may in fact be
passed a reference to a Derived object. If Derived is-not-A Base, then the function is likely
to behave unexpectedly (i.e. wrongly).
By re-using the base class test cases when testing the derived class(es), we can verify that the
isA relationship holds true, and that the derived class correctly implements the interfacedefined by the base class. This approach can even be used for abstract base classes: test cases
7/28/2019 C++ testing
10/13
IPL Information Processing Ltd C++: Its Testing, Jim, Page 10 of 13
November 1997 But Not As We Know It
can be written which verify the correct implementation of the interface provided by the
abstract base.
The above applies equally to re-use through template instantiation. Each template makes
certain assumptions about the types on which it will be instantiated: that they are copyable,
assignable, have an equality operation etc. These assumptions are both syntactic (obj1 ==
obj2 is legal) and semantic (== defines an equivalence relation). Unfortunately, there is no
way to make these assumptions explicit in the C++ language10. To verify that the assumptionshave not been violated, write a set of re-usable test cases for the template, and use those test
cases to test each instantiation.
Re-usable test cases will in general be behavioural tests, based on the externally visible
functionality of the class under test. It is also possible to re-use structural test cases, where the
code in question has not been replaced (i.e. for inherited members, or templates for which no
specialisation has been defined).
3.3.2 Factory Classes
Writing a re-usable test case is, in most respects the same as writing a normal test case. Extra
care is needed to ensure that no unwarranted assumptions are made about the types of objects.
For inheritance re-use, the test script must use references or pointers instead of simple objects.
For template re-use, the test script itself is a template, parameterised by the same types as the
template under test.
Most test cases create one or more objects, in preparation for the test proper. How can a re-
usable test script create objects, without knowing the type of the objects being tested. The
solution, again, is to use the Factory Method pattern.
The test case is passed a Factory object as a parameter, and uses the factory to create objects
as needed. A different Factory class is written for each class which is to be tested. Whenever
the test case is used to test a class, it is passed the corresponding Factory object.
To ensure that different Factory objects can be substituted as necessary, the Factory classes
form an inheritance hierarchy of their own, which precisely mirrors that of the classes to be
tested. This parallel inheritance hierarchy means that if a Circle isA Shape, then a
CircleFactory isA ShapeFactory.
In the following diagram, Shape is an abstract class, and Circle is derived (publicly) from
Shape. ShapeTest contains the test cases which test for Shape-ness. CircleTest
contains the test cases which test for Circle-ness. Both consist of re-usable test cases,
and CircleTest automatically runs the ShapeTest test cases on Circles, to ensure that
Circles are indeed Shapes.ShapeTest needs to create Shapes before it can test them; it uses a Shape-Factory
(passed as a reference parameter) to do this. Similarly, CircleTest needs to create
Circles, for which it uses a CircleFactory.
ShapeFactory is an abstract class, since it cant actually create any Shapes. The purpose
ofShapeFactory is to define an interface for shape-creation which the derived factory
classes (like CircleFactory) must implement.
10 c.f. the constrained genericity and design-by-contract ideas described in [Meyer] and supported by the Eiffel
language.
7/28/2019 C++ testing
11/13
IPL Information Processing Ltd C++: Its Testing, Jim, Page 11 of 13
November 1997 But Not As We Know It
CircleFactory implements the interface by creating Circles (remember that a
Circle isA Shape, so this fulfils the ShapeFactory requirements). A hypothetical
SquareFactory would implement the same interface by creating Squares.
CircleFactory also defines an additional interface specifically for creating Circles.
When CircleTest runs the ShapeTest test cases, it passes it the CircleFactory
object. This is used by the ShapeTest test cases, which test that the Circles so created
are valid Shapes.
In this section we have seen how to use re-usable test cases to keep testing effort for re-usablecomponents within reasonable bounds. But how do we ensure that a component has been
tested in all necessary contexts? This question, ofcoverage, is addressed in the next section.
3.4 Inheritance Context Coverage
Whenever a member function is inherited, it can be re-used in a new context acting on a
Derived class object instead of a Base class object. As discussed above, we need to re-test
such member functions in each new context (preferably using re-usable test cases).
To ensure that the member function has been thoroughly tested in each context, it is necessary
to extend the definition of existing structural coverage metrics to take into account the
inheritance context. For each metric, coverage is measured separately for each context. Thecurrent context is the (dynamic) type of the object on which the member function is acting.
The traditional coverage metrics can be calculated from the inheritance context coverage data
by aggregating the coverage achieved across all contexts.
When calculated for a specific inheritance context, function entry-point coverage provides a
basic verification that each function has been calculated in the specified context. Typical
recommended coverage requirements are:
1. once-full context coverage: 100% (statement, decision) coverage in at least one
context, 100% entry-point coverage in all contexts
2. strict context coverage: 100% (statement, decision) coverage in all contexts
Use of inheritance context coverage metrics can be used to ensure that re-used code has been
tested in all necessary contexts.
3.5 State Coverage
When testing a class whose behaviour is modelled by a state machine, we will naturally use
our knowledge of the possible states, and the transitions between them, to help design suitable
test cases.11
To ensure that we have not missed any cases, enhanced coverage metrics can be
used to measure coverage separately for each possible state.
11See [Binder] for a description of one such approach.
Shape(abstract)
Circle Square
ShapeTest
CircleTest
SquareTest
Shape Factory(abstract)
CircleFactory
SquareFactory
used to
create Shapes
used tocreate Circles
7/28/2019 C++ testing
12/13
IPL Information Processing Ltd C++: Its Testing, Jim, Page 12 of 13
November 1997 But Not As We Know It
For example, consider a bounded_stack class. It will have states empty, normal and
full. To ensure that the classs behaviour in each state has been tested, coverage is
measured separately for each state.
The user must provide a function which determines the current state. In all other respects,
state coverage is very similar to inheritance context coverage: coverage is measured separately
for each state/context, and coverage metrics can be calculated for a specific context, or across
all contexts.
Typical recommended coverage requirements are:
1. once-full state coverage: 100% (statement, decision) coverage in at least one state,
100% entry-point coverage in all states
2. strict state coverage: 100% (statement, decision) coverage in all states
Astute readers will have realised that achieving strict state coverage will often be infeasible.
If the class is to behave differently in each state, then there must be some code which is
executed differently, depending on the state. For example, in the bounded_stack class, the
push() member function is likely to include a decision of the form if (empty()).
Clearly, this decision will only branch true in the empty state, and will only branch false in
the normal or full states.
A structural coverage metric for which 100% coverage is infeasible is almost useless as a
measure of testedness since it is impossible to know what 75% coverage means: the 3
feasible cases have all been covered or 9 of the 11 feasible cases have been covered.
To counter this problem, the enhanced coverage metrics are further modified to allow the
tester to define particular coverage items (e.g. decision branches) as being infeasible in a
particular state. Coverage metrics can then be calculated based on the feasible cases only: in
the example above the results would be 100% and 82% coverage respectively making
the difference clear.
12
In the inheritance context case, achieving full coverage usually is feasible. Inherited member
functions should behave (mostly) the same when applied to a derived class as they do in the
base class; any differences should be encapsulated as virtual member functions which are
overridden. Thus, most member functions do not contain decisions based on the actual type
of the underlying object. However, in those rare cases (involving the dynamic_cast
operator) where there is a problem, the same approach can be applied.
4. Summary
We have seen that testing C++ software presents a number of new challenges. Increased
numbers of dependencies, combined with the exposition of class implementation details in
header files, make isolation testing infeasible in most cases. The resulting switch to bottom-
up testing forces us to perform unit-level testing on larger and larger integrations of units.
The re-use of components, through inheritance and instantiation, forces us to consider the re-
use of test cases. New coverage metrics are required to maintain the value of structural
coverage analysis for test case design.
This paper has presented solutions to all these problems. The use of tools implementing and
supporting these solutions, such as IPLs new product Cantata++ [IPL], will result in more
efficient and more effective C++ testing.
12To ensure that cheating is not possible, the raw coverage metric is also reported.
7/28/2019 C++ testing
13/13
IPL Information Processing Ltd C++: Its Testing, Jim, Page 13 of 13
November 1997 But Not As We Know It
5. References
[Binder] R. Binder, The FREE Approach to Testing Object-Oriented Software,
http://www.rbsc.com/pages/FREE.html
[Harrold] M.J. Harrold, J.D. McGregor, and K.J. Fitzpatrick, Incremental Testing of Object-
Oriented Class Structures, Proceedings of the Fourteenth International Conference on
Software Engineering, 1992, pp. 68 - 80.
[IPL] Cantata++ product information at http://www.iplbath.com
[Lakos] J. Lakos, Large Scale C++ Software Design, 3 part series starting in C++ Report
June 1996
[Martin] R.C. Martin, The Dependency Inversion Principle, C++ Report May 1996 (also
available at http://www.oma.com)
[Meyer] B. Meyer, Object-Oriented Software Construction (2ed
), Prentice Hall, ISBN 0-13-
629155-4