The class as a testable unit
If an
organization is using the object-oriented paradigm to develop software systems
it will need to select the component to be considered for unit test. As described
in Section 6.1, the choices consist of either the individual method as a unit
or the class as a whole. Each of these choices requires special consideration
on the part of the testers when designing and running the unit tests, and when
retesting needs to be done. For example, in the case of the method as the
selected unit to test, it may call other methods within its own class to
support its functionality. Additional code, in the form of a test harness, must
be built to represent the called methods within the class. Building such a test
harness for each individual method often requires developing code equivalent to
that already existing in the class itself (all of its other methods). This is
costly; however, the tester needs to consider that testing each individual
method in this way helps to ensure that all statements/branches have been
executed at least once, and that the basic functionality of the method is
correct. This is especially important for mission or safety critical methods.
In spite
of the potential advantages of testing each method individually, many
developers/testers consider the class to be the component of choice for unit
testing. The process of testing classes as units is sometimes called component
test . A class encapsulates multiple interacting methods operating on common
data, so what we are testing is the intraclass interaction of the methods. When
testing on the class level we are able detect not only traditional types of
defects, for example, those due to control or data flow errors, but also
defects due to the nature of objectoriented systems, for example, defects due
to encapsulation, inheritance, and polymorphism errors. We begin to also look
for what Chen calls object management faults, for example, those associated
with the instantiation, storage, and retrieval of objects .
This
brief discussion points out some of the basic trade-offs in selecting the
component to be considered for a unit test in object-oriented systems. If the
class is the selected component, testers may need to address special issues
related to the testing and retesting of these components. Some of these issues
are raised in the paragraphs that follow.
Issue 1: Adequately Testing Classes
The
potentially high costs for testing each individual method in a class have been
described. These high costs will be particularly apparent when there are many
methods in a class; the numbers can reach as high as 20 to 30. If the class is
selected as the unit to test, it is possible to reduce these costs since in
many cases the methods in a single class serve as drivers and stubs for one
another. This has the effect of lowering the complexity of the test harness
that needs to be developed. However, in some cases driver classes that
represent outside classes using the methods of the class under test will have
to be developed. In addition, if it is decided that the class is the smallest
component to test, testers must decide if they are able to adequately cover all
necessary features of each method in class testing. Some researchers believe
that coverage objectives and test data need to be developed for each of the
methods, for example, the create, pop,
push, empty, full, and show_top
methods associated with the stack class shown in Figure 6.3. Other researchers
believe that a class can be adequately tested as a whole by observation of
method interactions using
a
sequence of calls to the member functions with appropriate parameters.
Again,
referring to the stack class shown in Figure 6.3, the methods push, pop, full, empty, and show_top will either read or modify the
state of the stack. When testers unit (or component) test this class what they
will need to focus on is the operation of each of the methods in the class and
the interactions between them. Testers will want to determine, for example, if push places an item in the correct
position at the top of the stack. However, a call to the method full may have to be made first to
determine if the stack is already full. Testers will also want to determine if push and pop work together properly so that the stack pointer is in the
correct position after a sequence of calls to these methods. To properly test
this class, a sequence of calls to the methods needs to be specified as part of
component test design. For example, a test sequence for a stack that can hold
three
items might be:
create(s,3),
empty(s), push(s,item-1), push(s,item-2), push(s,item-3), full(s), show_top(s),
pop(s,item), pop(s,item), pop(s,item), empty(s), .
The
reader will note that many different sequences and combination of calls are
possible even for this simple class. Exhaustively testing every possible
sequence is usually not practical. The tester must select those sequences she
believes will reveal the most defects in the class. Finally, a tester might use
a combination of approaches, testing some of the critical methods on an
individual basis as units, and then testing the class as a whole.
Issue 2: Observation of Object States and State
Changes
Methods
may not return a specific value to a caller. They may instead change the state
of an object. The state of an object is represented by a specific set of values
for its attributes or state variables. State-based testing as described in
Chapter 4 is very useful for testing objects.
Methods
will often modify the state of an object, and the tester must ensure that each
state transition is proper. The test designer can prepare a state table (using
state diagrams developed for the requirements specification) that specifies
states the object can assume, and then in the table indicate sequence of
messages and parameters that will cause the object to enter each state. When
the tests are run the tester can enter results in this same type of table. For
example, the first call to the method push
in the stack class of Figure 6.3, changes the state of the stack so that empty is no longer true. It also changes
the value of the stack pointer variable, top.
To determine if the method push is
working properly the value of the variable
top must be visible both before and after the invocation of this method. In this case the method show_top within the class may be called
to perform this task. The methods full
and empty also probe the state of the
stack. A sample augmented sequence of calls to check the value of top and the full/empty state of the three-item
stack is:
empty(s),
push(s,item-1), show _top(s), push(s,item-2), show_top(s), push(s,item-3),
full(s), show_top(s), pop(s,item), show_top(s), pop(s,item), show_top(s),
empty(s), . . .
Issue 3: The Retesting of Classes—I
One of
the most beneficial features of object-oriented development is encapsulation.
This is a technique that can be used to hide information. A program unit, in
this case a class, can be built with a well-defined public interface that proclaims
its services (available methods) to client classes. The implementation of the
services is private. Clients who use the services are unaware of implementation
details. As long as the interface is unchanged, making changes to the
implementation should not affect the client classes. A tester of
object-oriented code would therefore conclude that only the class with
implementation changes to its methods needs to be retested. Client classes
using unchanged interfaces need not be retested. In an object-oriented system,
if a developer changes a class implementation that class needs to be retested
as well as all the classes that depend on it. If a superclass, for example, is
changed, then it is necessary to retest all of its subclasses. In addition,
when a new subclass is added (or modified), we must also retest the methods
inherited from each of its ancestor superclasses. The new (or changed) subclass
introduces an unexpected form of dependency because there now exists a new
context for the inherited components.
Issue 4: The Retesting of Classes—II
Classes
are usually a part of a class hierarchy where there are existing inheritance
relationships. Subclasses inherit methods from their superclasses. Very often a
tester may assume that once a method in a superclass has been tested, it does
not need retested in a subclass that inherits it. However, in some cases the
method is used in a different context by the subclass and will need to be
retested. In addition, there may be an overriding of methods where a subclass
may replace an inherited method with a locally defined method. Not only will
the new locally defined method have to be retested, but designing a new set of
test cases may be necessary. This is because the two methods (inherited and
new) may be structurally different. The antiextentionality axiom as discussed
in Chapter 5 expresses this need .
The
following is an example of such as case using the shape class in Figure 6.4.
Suppose the shape superclass has a subclass, triangle, and triangle has a
subclass, equilateral triangle. Also suppose that the method display in shape needs to call the
method color for its operation.
Equilateral triangle could have a local definition for the method display. That method could in turn use a
local definition for color which has
been defined in triangle. This local definition of the color method in triangle has been tested to work with the inherited
display method in shape, but not with
the locally defined display in
equilateral triangle. This is a new context that must be retested. A set of new
test cases should be developed. The tester must carefully examine all the
relationships between members of a class to detect such occurrences.
Related Topics
Privacy Policy, Terms and Conditions, DMCA Policy and Compliant
Copyright © 2018-2023 BrainKart.com; All Rights Reserved. Developed by Therithal info, Chennai.