0% found this document useful (0 votes)
13 views169 pages

OOSE Unit 5 Nandita

The document outlines Unit 5 of the OOSE Based Application Development course, focusing on coding and testing, mapping models to code, and various transformation principles. Key topics include optimization of object design models, refactoring, forward and reverse engineering, and the importance of validation in transformations. It emphasizes the need for careful mapping of models to code and the management of testing processes to ensure system functionality and efficiency.

Uploaded by

ppamarth2
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
13 views169 pages

OOSE Unit 5 Nandita

The document outlines Unit 5 of the OOSE Based Application Development course, focusing on coding and testing, mapping models to code, and various transformation principles. Key topics include optimization of object design models, refactoring, forward and reverse engineering, and the importance of validation in transformations. It emphasizes the need for careful mapping of models to code and the management of testing processes to ensure system functionality and efficiency.

Uploaded by

ppamarth2
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd

GITAM School of Technology

19EAI334: OOSE Based Application


Development

By
Dr. Nandita Bhanja Chaudhuri
Unit 5 Syllabus
• Coding & Testing
• Mapping Models to Code
• Mapping Concepts Model Transformation, Refactoring, Forward Engineering,
Reverse Engineering,Transformation Principles, Mapping Activities: Optimizing
the Object Design Model, Mapping Associations to Collections, Mapping
Contracts to Exceptions, Mapping Object Models to a Persistent Storage Schema
• Testing:
• An Overview of Testing, Testing Concepts: Faults, Erroneous States, and Failures,
Test Cases, Test Stubs and Drivers, Corrections Testing Activities: Component
Inspection, Usability Testing, Unit Testing, Integration Testing, System Testing
Managing Testing: Planning Testing, Documenting Testing, Assigning
Responsibilities, Regression Testing, Automating Testing
• Documenting Architecture: Architectural views: logical, deployment, security,
data, behavioural.
Mapping Models to Code
• Optimizing the class model
• Mapping associations to collections
• Mapping operation contracts to exceptions
• Mapping the class model to a storage schema.
An Overview of Mapping
A transformation aims at improving one aspect of the model (e.g., its modularity)
while preserving all of its other properties (e.g., its functionality). Hence, a
transformation is usually localized, affects a small number of classes, attributes, and
operations, and is executed in a series of small steps. These transformations occur
during numerous object design and implementation activities. We focus in detail on
the following activities:
• Optimization. This activity addresses the performance requirements of the
system model. This includes reducing the multiplicities of associations to speed
up queries, adding redundant associations for efficiency, and adding derived
attributes to improve object access time.
• Realizing associations. During this activity, we map associations to source code
constructs, such as references and collections of references.
• Mapping contracts to exceptions. During this activity, we describe the behavior
of operations when contracts are broken. This includes raising exceptions when
violations are detected and handling exceptions in higher level layers of the
system.
• Mapping class models to a storage schema. During system design, we selected a
persistent storage strategy, such as a database management system, a set of flat
files, or a combination of both. During this activity, we map the class model to a
storage schema, such as a relational database schema.
Mapping Concepts
We distinguish four types of transformations (Figure 10-1):

Figure 10-1 The four types of transformations described in this chapter: model transformations, refactorings,
forward engineering, and reverse engineering.
• Model transformations operate on object models. An example is the conversion
of a simple attribute (e.g., an address represented as a string) to a class (e.g., a
class with street address, zip code, city, state, and country attributes).
• Refactorings are transformations that operate on source code. They are similar to
object model transformations in that they improve a single aspect of the system
without changing its functionality. They differ in that they manipulate the source
code.
• Forward engineering produces a source code template that corresponds to an
object model. Many modeling constructs, such as attribute and association
specifications, can be mechanically mapped to source code constructs supported
by the selected programming language (e.g., class and field declarations in Java),
while the bodies and additional private methods are added by developers.
• Reverse engineering produces a model that corresponds to source code. This
transformation is used when the design of the system has been lost and must be
recovered from the source code. Although several CASE tools support reverse
engineering, much human interaction is involved for recreating an accurate model,
as the code does not include all information needed to recover the model
unambiguously.
Model Transformation
• A model transformation is applied to an object model and results in another
object model [Blaha & Premerlani, 1998]. The purpose of object model
transformation is to simplify or optimize the original model, bringing it into closer
compliance with all requirements in the specification. A transformation may
add, remove, or rename classes, operations, associations, or attributes. A
transformation can also add information to the model or remove information from
it.
• Analysis, we used transformations to organize objects into inheritance
hierarchies and eliminate redundancy from the analysis model. For example,
the transformation in Figure 10-2 takes a class model with a number of classes that
contain the same attribute and removes the redundancy. The Player, Advertiser,
and LeagueOwner in ARENA all have an email address attribute. We create a
superclass User and move the email attribute to the superclass.
• In principle, the development process can be thought as a series of model
transformations, starting with the analysis model and ending with the object
design model, adding solution domain details along the way. Although applying a
model transformation is a fairly mechanical activity, identifying which
transformation to apply to which set of classes requires judgement and experience.
Refactoring
• A refactoring is a transformation of the source code that improves its
readability or modifiability without changing the behavior of the system
[Fowler, 2000]. Refactoring aims at improving the design of a working system
by focusing on a specific field or method of a class. To ensure that the
refactoring does not change the behavior of the system, the refactoring is done in
small incremental steps that are interleaved with tests. The existence of a test
driver for each class allows developers to confidently change the code and
encourages them to change the interface of the class as little as possible during the
refactoring.
• For example, the object model transformation of Figure 10-2 corresponds to a
sequence of three refactorings. The first one, Pull Up Field, moves the email field
from the subclasses to the superclass User. The second one, Pull Up Constructor
Body, moves the initialization code from the subclasses to the superclass. The
third and final one, Pull Up Method, moves the methods manipulating the email
field from the subclasses to the superclass. Let’s examine these three refactorings
in detail.
Figure 10-2 An example of an object model transformation. A redundant attribute can be eliminated by
creating a superclass.
• Pull Up Field relocates the email field using the following steps (Figure 10-3):
1. Inspect Player, LeagueOwner, and Advertiser to ensure that the email field is
equivalent. Rename equivalent fields to email if necessary.
2. Create public class User.
3. Set parent of Player, LeagueOwner, and Advertiser to User.
4. Add a protected field email to class User.
5. Remove fields email from Player, LeagueOwner, and Advertiser.
6. Compile and test.
Then, we apply the Pull Up Constructor Body refactoring to move the initialization
code for email using the following steps (Figure 10-4):
1. Add the constructor User(Address email) to class User.
2. Assign the field email in the constructor with the value passed in the parameter.
3. Add the call super(email) to the Player class constructor.
4. Compile and test.
5. Repeat steps 1–4 for the classes LeagueOwner and Advertiser.
• At this point, the field email and its corresponding initialization code are in the
User class. Now, we examine if methods using the email field can be moved from
the subclasses to the User class. To achieve this, we apply the Pull Up Method
refactoring:

1. Examine the methods of Player that use the email field. Note that [Link]()
uses email and that it does not use any fields or operations that are specific to Player.
2. Copy the [Link]() method to the User class and recompile.
3. Remove the [Link]() method
4. Compile and test.
5. Repeat for LeagueOwner and Advertiser.
• Applying these three refactorings effectively transforms the ARENA source code
in the same way the object model transformation of Figure 10-2 transformed the
ARENA object design model. Note that the refactorings include many more steps
than its corresponding object model transformation and interleave testing with
changes. This is because the source code includes many more details, so it
provides many more opportunities for introducing errors. In the next section, we
discuss general principles for avoiding transformation errors.
Forward Engineering
• Forward engineering is applied to a set of model elements and results in a set
of corresponding source code statements, such as a class declaration, a Java
expression, or a database schema. The purpose of forward engineering is to
maintain a strong correspondence between the object design model and the code,
and to reduce the number of errors introduced during implementation, thereby
decreasing implementation effort.
• For example, Figure 10-5 depicts a particular forward engineering transformation
applied to the classes User and LeagueOwner. First, each UML class is mapped to
a Java class. Next, the UML generalization relationship is mapped to an extends
statement in the LeagueOwner class. Finally, each attribute in the UML model is
mapped to a private field in the Java classes and to two public methods for setting
and getting the value of the field. Developers can then refine the result of the
transformation with additional behavior, for example, to check that the new value
of maxNumLeagues is a positive integer.
Figure 10-5 Realization of the User and LeagueOwner classes (UML class diagram and Java excerpts). In this
transformation, the public visibility of email and maxNumLeagues denotes that the methods for getting and setting
their values are public. The actual fields representing these attributes are private.
• Note that, except for the names of the attributes and methods, the code resulting
from this transformation is always the same. This makes it easier for developers to
recognize transformations in the source code, which encourages them to comply
with naming conventions. Moreover, since developers use one consistent approach
for realizing classes, they introduce fewer errors.
Reverse Engineering
• Reverse engineering is applied to a set of source code elements and results in
a set of model elements. The purpose of this type of transformation is to recreate
the model for an existing system, either because the model was lost or never
created, or because it became out of sync with the source code. Reverse
engineering is essentially an inverse transformation of forward engineering.
Reverse engineering creates a UML class for each class declaration statement,
adds an attribute for each field, and adds an operation for each method. However,
because forward engineering can lose information (e.g., associations are turned
into collections of references), reverse engineering does not necessarily recreate
the same model. Although many CASE tools support reverse engineering, CASE
tools provide, at best, an approximation that the developer can use to rediscover
the original model.
Transformation Principles
• A transformation aims at improving the design of the system with respect to
some criterion. We discussed four types of transformations so far: model
transformations, refactorings, forward engineering, and reverse engineering.
A model transformation improves the compliance of the object design model with
a design goal. A refactoring improves the readability or the modifiability of the
source code. Forward engineering improves the consistency of the source code
with respect to the object design model. Reverse engineering tries to discover the
design behind the source code.
• However, by trying to improve one aspect of the system, the developer runs the
risk of introducing errors that will be difficult to detect and repair. To avoid
introducing new errors, all transformations should follow these principles:
• Each transformation must address a single criteria. A transformation should
improve the system with respect to only one design goal. One transformation can
aim to improve response time. Another transformation can aim to improve
coherence. However, a transformation should not optimize multiple criteria. If
you find yourself trying to deal with several criteria at once, you most likely
introduce errors by making the source code too complex.
• Each transformation must be local. A transformation should change only a few
methods or a few classes at once. Transformations often target the
implementation of a method, in which case the callers are not affected. If a
transformation changes an interface (e.g., adding a parameter to a method), then
the client classes should be changed one at the time (e.g., the older method should
be kept around for background compatibility testing). If you find yourself
changing many subsystems at once, you are performing an architectural change,
not an object model transformation.
• Each transformation must be applied in isolation to other changes. To further
localize changes, transformations should be applied one at the time. If you are
improving the performance of a method, you should not add new
functionality. If you are adding new functionality, you should not optimize
existing code. This enables you to focus on a limited set of issues and reduces the
opportunities for errors.
• Each transformation must be followed by a validation step. Even though
transformations have a mechanical aspect, they are applied by humans. After
completing a transformation and before initiating the next one, validate the
changes. If you applied an object model transformation, update the sequence
diagrams in which the classes under consideration are involved. Review the use
cases related to the sequence diagrams to ensure that the correct functionality is
provided. If you applied a refactoring, run the test cases relevant to the classes
under consideration. If you added new control statements or dealt with new
boundary cases, write new tests to exercise the new source code. It is always
easier to find and repair a bug shortly after it was introduced than later.
Mapping Activities
In this section, we present transformations that occur frequently to illustrate the
principles we described in the previous section. We focus on transformations during
the following activities:
• Optimizing the Object Design Model
• Mapping Associations to Collections
• Mapping Contracts to Exceptions
• Mapping Object Models to a Persistent Storage Schema
Optimizing the Object Design Model
• The direct translation of an analysis model into source code is often inefficient.
The analysis model focuses on the functionality of the system and does not take
into account system design decisions. During object design, we transform the
object model to meet the design goals identified during system design, such as
minimization of response time, execution time, or memory resources. For
example, in the case of a Web browser, it might be clearer to represent HTML
documents as aggregates of text and images. However, if we decided during
system design to display documents as they are retrieved, we may introduce a
proxy object to represent placeholders for images that have not yet been retrieved.
• In this section, we describe four simple but common optimizations: adding
associations to optimize access paths, collapsing objects into attributes,
delaying expensive computations, and caching the results of expensive
computations.
• When applying optimizations, developers must strike a balance between efficiency
and clarity. Optimizations increase the efficiency of the system but also the
complexity of the models, making it more difficult to understand the system.
Optimizing access paths
Common sources of inefficiency are the repeated traversal of multiple
associations, the traversal of associations with “many” multiplicity, and the
misplacement of attributes [Rumbaugh et al., 1991].
• Repeated association traversals. To identify inefficient access paths, you should
identify operations that are invoked often and examine, with the help of a
sequence diagram, the subset of these operations that requires multiple
association traversal. Frequent operations should not require many traversals, but
should have a direct connection between the querying object and the queried
object. If that direct connection is missing, you should add an association between
these two objects. In interface and reengineering projects, estimates for the
frequency of access paths can be derived from the legacy system. In greenfield
engineering projects, the frequency of access paths is more difficult to estimate. In
this case, redundant associations should not be added before a dynamic analysis of
the full system—for example, during system testing—has determined which
associations participate in performance bottlenecks.
• “Many” associations. For associations with “many” multiplicity, you should try
to decrease the search time by reducing the “many” to “one.” This can be done
with a qualified association (Section 2.4.2). If it is not possible to reduce the
multiplicity of the association, you should consider ordering or indexing the
objects on the “many” side to decrease access time.
• Misplaced attributes. Another source of inefficient system performance is
excessive modeling. During analysis many classes are identified that turn out to
have no interesting behavior. If most attributes are only involved in set() and
get() operations, you should reconsider folding these attributes into the calling
class. After folding several attributes, some classes may not be needed anymore
and can simply removed from the model.
• The systematic examination of the object model using the above questions should
lead to a model with selected redundant associations, with fewer inefficient many-
to-many associations, and with fewer classes.
Collapsing objects: Turning objects into attributes
• After the object model is restructured and optimized a couple of times, some of its
classes may have few attributes or behaviors left. Such classes, when associated
only with one other class, can be collapsed into an attribute, thus reducing the
overall complexity of the model.
• Consider, for example, a model that includes Persons identified by a
SocialSecurity object. During analysis, two classes may have been identified. Each
Person is associated with a SocialSecurity class, which stores a unique social
security number identifying the Person. Now, assume that the use cases do not
require any behavior for the SocialSecurity object and that no other classes have
associations with the SocialSecurity class. In this case, the SocialSecurity class
should be collapsed into an attribute of Person (see Figure 10-6).
Figure 10-6 Collapsing an object without interesting behavior into an attribute (UML class diagram).
• The decision of collapsing classes is not always obvious. In the case of a social
security system, the SocialSecurity class may have much more behavior, such as
specialized routines for generating new numbers based on birth dates and the
location of the original application. In general, developers should delay collapsing
decisions until the beginning of the implementation, when responsibilities for each
class are clear. Often, this occurs after substantial coding has occurred, in which
case it may be necessary to refactor the code.
The refactoring equivalent to the model transformation of Figure 10-6 is Inline Class
refactoring [Fowler, 2000]:
1. Declare the public fields and methods of the source class (e.g., SocialSecurity) in
the absorbing class (e.g., Person).
2. Change all references to the source class to the absorbing class.
3. Change the name of the source class to another name, so that the compiler catches
any dangling references.
4. Compile and test.
5. Delete the source class.
Delaying expensive computations
• Often, specific objects are expensive to create. However, their creation can
often be delayed until their actual content is needed. For example, consider an
object representing an image stored as a file (e.g., an ARENA
AdvertisementBanner). Loading all the pixels that constitute the image from the
file is expensive. However, the image data need not be loaded until the image is
displayed. We can realize such an optimization using a Proxy design pattern
[Gamma et al., 1994]. An ImageProxy object takes the place of the Image and
provides the same interface as the Image object (Figure 10-7). Simple operations
such as width() and height() are handled by ImageProxy. When Image needs to be
drawn, however, ImageProxy loads the data from disk and creates a RealImage
object. If the client does not invokes the paint() operation, the RealImage object is
not created, thus saving substantial computation time. The calling classes only
access the ImageProxy and the RealImage through the Image interface.
Figure 10-7 Delaying expensive computations to transform the object design model using a Proxy design pattern
(UML class diagram).
Caching the result of expensive computations
• Some methods are called many times, but their results are based on values that
do not change or change only infrequently. Reducing the number of
computations required by these methods substantially improve overall response
time. In such cases, the result of the computation should be cached as a private
attribute. Consider, for example, the [Link]() operation,
which displays the statistics relevant to all Players and Tournaments in a League.
These statistics change only when a Match is completed, so it is not necessary to
recompute the statistics every time a User wishes to see them. Instead, the
statistics for a League can be cached in a temporary data structure, which is
invalidated the next time a Match is completed. Note that this approach includes a
time-space trade-off: we improve the average response time for the getStatistics()
operation, but we consume memory space by storing redundant information.
Mapping Associations to Collections
• Associations are UML concepts that denote collections of bidirectional links
between two or more objects. Object-oriented programming languages,
however, do not provide the concept of association. Instead, they provide
references, in which one object stores a handle to another object, and
collections, in which references to several objects can be stored and possibly
ordered. References are unidirectional and take place between two objects.
During object design, we realize associations in terms of references, taking into
account the multiplicity of the associations and their direction.
• Unidirectional one-to-one associations.
• The simplest association is a unidirectional one-to-one association. For example (Figure
10-8), in ARENA, an Advertiser has a one-to-one association with an Account object that
tracks all the charges accrued from displaying AdvertisementBanners. This association is
unidirectional, as the Advertiser calls the operations of the Account object, but the
Account never invokes operations of the Advertiser. In this case, we map this
association to code using a reference from the Advertiser to the Account. That is, we add
a field to Advertiser named account of type Account.
• Creating the association between Advertiser and Account translates to setting the account
field to refer to the correct Account object. Because each Advertiser object is associated
with exactly one Account, a null value for the account attribute can only occur when a
Advertiser object is being created. Otherwise, a null account is considered an error. Since
the reference to the Account object does not change over time, we make the account field
private and add a public [Link]() method. This prevents callers from
accidentally modifying the account field.
Figure 10-8 Realization of a unidirectional, one-to-one association (UML class diagram and Java)
• Bidirectional one-to-one associations.
• The direction of an association often changes during the development of the
system. Unidirectional associations are simple to realize. Bidirectional
associations are more complex and introduce mutual dependencies among classes.
Assume that we modify the Account class so that the display name of the Account
is computed from the name of the Advertiser. In this case, an Account needs to
access its corresponding Advertiser object. Consequently, the association between
these two objects must be bidirectional (Figure 10-9). We add an owner attribute
to Account in the Java source code, but this is not sufficient: by adding a second
attribute to realize the association, we introduce redundancy into the model. We
need to ensure that if a given Account has a reference to a specific Advertiser, the
Advertiser has a reference to that same Account. In this case, as the Account
object is created by the Advertiser constructor, we add a parameter to the Account
constructor to initialize the owner field to the correct value.
• Thus, the initial values for both fields are specified in the same statement in the
Advertiser constructor. Moreover, we make the owner field of Account private and
add a public method to get its value. Since neither the Advertiser class nor the
Account class modifies the field anywhere else, this ensures that both reference
attributes remain consistent. Note that this assumption is not enforceable with the
programming language constraints. The developer needs to document this
assumption by writing a one-line comment immediately before the account and
owner fields.
• In Figure 10-9, both the Account and the Advertiser classes must be recompiled
and tested whenever we change either class. With a unidirectional association
from the Advertiser class to the Account class, the Account class would not be
affected by changes to the Advertiser class. Bidirectional associations, however,
are usually necessary in the case of classes that need to work together closely. The
choice between unidirectional or bidirectional associations is a trade-off to be
evaluated in each specific context. To make the trade-off easier, we can
systematically make all attributes private and provide corresponding getAttribute()
and setAttribute() operations to access the reference. This minimizes changes to
APIs when changing a unidirectional association to bidirectional or vice versa.
Figure 10-9 Realization of a bidirectional one-to-one association (UML class diagram and Java excerpts).
• One-to-many associations.
• One-to-many associations cannot be realized using a single reference or a pair of
references. Instead, we realize the “many” part using a collection of references.
For example, assume that an Advertiser can have several Accounts to track the
expenses accrued by AdvertisementBanners for different products. In this case, the
Advertiser object has a one-to-many association with the Account class (Figure
10-10). Because Accounts have no specific order and because an Account can be
part of an Advertiser at most once, we use a set of references, called accounts, to
model the “many” part of the association. Moreover, we decide to realize this
association as a bidirectional association, and so add the addAccount(),
removeAccount(), and setOwner() methods to the Advertiser and Account classes
to update the accounts and owner fields.
• As in the one-to-one example, the association must be initialized when Advertiser
and Account objects are created. However, since an Advertiser can have a varying
number of Accounts, the Advertiser object does not invoke the Account
constructor. Instead, a control object for creating and archiving Accounts is
responsible for invoking the constructor.
Figure 10-10 Realization of a bidirectional, one-to-many association (UML class diagram and Java).
• Many-to-many associations.
• In this case, both end classes have fields that are collections of references and
operations to keep these collections consistent. For example, the Tournament class
of ARENA has an ordered many-to-many association with the Player class. This
association is realized by using a List attribute in each class, which is modified by
the operations addPlayer(), removePlayer(), addTournament(), and
removeTournament() (Figure 10-11). We already identified acceptPlayer() and
removePlayer() operations in the object design model (see Figure 9-11). We
rename acceptPlayer() to addPlayer() to maintain consistency with the code
generated for other associations.
Figure 10-11 Realization of a bidirectional, many-to-many association (UML class diagram and Java).
• Qualified associations.
• Modeling with UML, qualified associations are used to reduce the multiplicity
of one “many” side in a one-to-many or a many-to-many association. The
qualifier of the association is an attribute of the class on the “many” side of the
association, such as a name that is unique within the context of the association, but
not necessarily globally unique. For example, consider the association between
League and Player (Figure 10-12). It is originally a many-to-many association (a
League involves many Players, a Player can take part in many Leagues). To make
it easier to identify Players within a League, Players can choose a short nickname
that must be unique within the League. However, the Player can choose different
nicknames in different Leagues, and the nicknames do not need to be unique
globally within an Arena.
Figure 10-12 Realization of a bidirectional qualified association (UML class diagram; arrow denotes the successive
transformations).
• Qualified associations are realized differently from the way one-to-many and
many-tomany associations are realized. The main difference is that we use a Map
object to represent the qualified end, as opposed to a List or a Set, and we pass the
qualifier as a parameter in the operations to access the other end of the association.
To continue our example, consider the association between League and Player. We
realize this qualified association by creating a private players attribute in League
and a leagues attribute in Player. The players attribute is a Map indexed by the
nickname of the Player within the League. Because the nickname is stored in the
Map, a specific Player can have different nicknames across Leagues. The players
attribute is modified with the operations addPlayer() and removePlayer(). A
specific Player is accessed with the getPlayer() with a specific nickName, which
reduces the need for iterating through the Map to find a specific Player. The other
end of the association is realized with a Set, as before.
• Associations classes.
• In UML, we use an association class to hold the attributes and operations of an
association. For example, we can represent the Statistics for a Player within a
Tournament as an association class, which holds statistics counters for each
Player/Tournament combination (Figure 10-13). To realize such an association, we
first transform the association class into a separate object and a number of binary
associations. Then we can use the techniques discussed earlier to convert each
binary association to a set of reference attributes. In Section 10.6, we revisit this
case and describe additional mappings for realizing association classes.
• Once associations have been mapped to fields and methods, the public interface of
classes is relatively complete and should change only as a result of new
requirements, discovered bugs, or refactoring.
Figure 10-13 Transformation of an association class into an object and two binary associations (UML class diagram).
Mapping Contracts to Exceptions
• Object-oriented languages that include constraints, such as Eiffel, can automatically check
contracts and raise exceptions when a contract is violated. This enables a class user to
detect bugs associated with incorrect assumptions about the used class. In particular, this
is useful when developers reuse a set of classes to discover boundary cases. Raising
exceptions when postconditions are violated enables class implementors to catch bugs
early, to identify precisely the operation in which the violation occurred, and to correct the
offending code.
• Unfortunately, many object-oriented languages, including Java, do not provide built-in
support for contracts. However, we can use their exception mechanisms as building
blocks for signaling and handling contract violations. In Java, we raise an exception with
the throw keyword followed by an exception object. The exception object provides a
place holder for storing information about the exception, usually an error message and
a backtrace representing the call stack of the throw.
• The effect of throwing an exception interrupts the control flow and unwinds the
call stack until a matching catch statement is found. The catch statement is
followed by a parameter bound to the exception object and an exception handling
block. If the exception object is of the same type of the parameter (or a subclass
thereof), the catch statement matches and the exception handling block is
executed.
• For example, in Figure 10-14, let us assume that the acceptPlayer() operation of
TournamentControl is invoked with a player who is already part of the
Tournament. In this case, [Link]() throws an exception of
type KnownPlayer, which is caught by the caller, [Link](),
which forwards the exception to the ErrorConsole class, and then proceeds with
the next Player. The ErrorConsole boundary object then displays a list of error
messages to the user.
Figure 10-14 Example of exception handling in Java. TournamentForm catches exceptions raised by Tournament
and TournamentControl and logs them into an error console for display to the user.
A simple mapping would be to treat each operation in the contract individually and
to add code within the method body to check the preconditions, postconditions, and
invariants relevant to the operation:
• Checking preconditions. Preconditions should be checked at the beginning of
the method, before any processing is done. There should be a test that checks if
the precondition is true and raises an exception otherwise. Each precondition
corresponds to a different exception, so that the client class can not only detect
that a violation occurred, but also identify which parameter is at fault.
• Checking postconditions. Postconditions should be checked at the end of the
method, after all the work has been accomplished and the state changes are
finalized. Each postcondition corresponds to a Boolean expression in an if
statement that raises an exception if the contract is violated. If more than one
postcondition is not satisfied, only the first detection is reported.
• Checking invariants. When treating each operation contract individually,
invariants are checked at the same time as postconditions.
• Dealing with inheritance. The checking code for preconditions and
postconditions should be encapsulated into separate methods that can be called
from subclasses.

• A systematic application of the above rules to the [Link]() contract


yields the code in Figure 10-15.
• If we mapped every contract following the above steps, we would ensure that all
preconditions, postconditions, and invariants are checked for every method
invocation, and that violations are detected within one method invocation. While
this approach results in a robust system (assuming the checking code is correct), it
is not realistic:
Figure 10-15 A complete implementation of the
[Link]() contract.
• Coding effort. In many cases, the code required for checking preconditions
and postconditions is longer and more complex than the code accomplishing the
real work. This results in increased effort that could be better spent in testing or
code clean-up.
• Increased opportunities for defects. Checking code can also include errors,
increasing testing effort. Worse, if the same developer writes the method and the
checking code, it is highly probable that bugs in the checking code mask bugs in
the actual method, thereby reducing the value of the checking code.
• Obfuscated code. Checking code is usually more complex than its corresponding
constraint and difficult to modify when constraints change. This leads to the
insertion of many more bugs during changes, defeating the original purpose of the
contract.
• Performances drawback. Checking systematically all contracts can significantly
slow down the code, sometimes by an order of magnitude. Although correctness
is always a design goal, response time and throughput design goals would not be
met.
Mapping Object Models to a Persistent
Storage Schema
• In this section, we look at the steps involved in mapping an object model to a relational
database using Java and database schemas.
• A schema is a description of the data, that is, a meta-model for data [Date, 2004]. In
UML, class diagrams are used to describe the set of valid instances that can be created by
the source code. Similarly, in relational databases, the database schema describes the
valid set of data records that can be stored in the database. Relational databases store
both the schema and the data. Relational databases store persistent data in the form of
tables (also called relations in the database literature). A table is structured in columns,
each of which represents an attribute. For example, in Figure 10-16, the User table has
three columns, firstName, login, and email. The rows of the table represent data records,
with each cell in the table representing the value of the attribute for the data record in that
row. In Figure 10-16, the User table contains three data records each representing the
attributes of specific users Alice, John, and Bob
Figure 10-16 An example of a relational table, with three attributes and three data records.
• A primary key of a table is a set of attributes whose values uniquely identify the
data records in a table. The primary key is used to refer unambiguously to a
specific data record when inserting, updating, or removing it. For example, in
Figure 10-16, the login attribute represents a unique user name within an Arena.
Hence, the login attribute can be used as a primary key. Note, however, the email
attribute is also unique across all users in the table. Hence, the email attribute
could also be used as a primary key. Sets of attributes that could be used as a
primary key are called candidate keys. Only the actual candidate key that is used
in the application to identify data records is the primary key.
• A foreign key is an attribute (or a set of attributes) that references the primary key
of another table. A foreign key links a data record in one table with one or more
data records in another table. In Figure 10-17, the table League includes the
foreign key owner that references the login attribute in the User table in Figure 10-
16. Alice is the owner of the tictactoeNovice and tictactoeExpert leagues and John
is the owner of the chessNovice league.
Figure 10-17 An example of a foreign key. The owner attribute in the League table refers to the primary key
of the User table in Figure 10-16.
Mapping classes and attributes
• When mapping the persistent objects to relational schemata, we focus first on the
classes and their attributes. We map each class to a table with the same name.
For each attribute, we add a column in the table with the name of the attribute in
the class. Each data record in the table corresponds to an instance of the class. By
keeping the names in the object model and the relational schema consistent, we
provide traceability between both representations and make future changes easier.
• When mapping attributes, we need to select a data type for the database column.
For primitive types, the correspondence between the programming language type
and the database type is usually trivial (e.g., the Java Date type maps to the
datetime type in SQL). However, for other types, such as String, the mapping is
more complex. The type text in SQL requires a specified maximum size. For
example, when mapping the ARENA User class, we could arbitrarily limit the
length of first names to 25 characters, enabling us to use a column of type
text[25]. Note that we have to ensure that users’ first names comply with this new
constraint by adding preconditions and checking code in the entity and boundary
objects.
• Next, we focus on the primary key. There are two options when selecting a
primary key for the table. The first option is to identify a set of class attributes
that uniquely identifies the object. The second option is to add a unique
identifier attribute that we generate.
• For example, in Figure 10-16, we use the login name of the user as a primary key.
Although this approach is intuitive, it has several drawbacks. If the value of the
login attribute changes, we need to update all tables in which the user login name
occurs as a foreign key. Also, selecting attributes from the application domain can
make it difficult to change the database schema when the application domain
changes. For example, in the future, we could use a single table to store users from
different Arenas. As login names are unique only within a single Arena, we would
need to add the name of the Arena in the primary key.
• The second option is to use an arbitrarily unique identifier (id) attribute as a
primary key. We generate the id attribute for each object and can guarantee that it
is unique and will not change. Some database management systems provide
features for automatically generating ids. This results in a more robust schema and
primary and foreign keys that consist of one column.
• For example, let us focus on the User class in ARENA (Figure 10-18). We map it
to a User table with four columns: id, firstName, login, and email. The type of the
id column is a long integer that we increment every time we create a new object.

Figure 10-18 Forward engineering of the User class to a database table.


Mapping associations
• After having mapped the classes to relational tables, we now turn to the mapping of
associations. The mapping of associations to a database schema depends on the
multiplicity of the association. One-to-one and one-to-many associations are implemented
as a so-called buried association [Blaha & Premerlani, 1998], using a foreign key. Many-
to-many associations are implemented as a separate table.
• Buried associations. Associations with multiplicity one can be implemented using a
foreign key. For one-to-many associations, we add a foreign key to the table
representing the class on the “many” end. For all other associations, we can select either
class at the end of the association. For example (Figure 10-19), consider the one-to-many
association between LeagueOwner and League. We map this association by adding a
owner column to the League table referring to the primary key of the LeagueOwner table.
The value of the owner column is the value of the id (i.e., the primary key) of the
corresponding league. If there are multiple Leagues owned by the same LeagueOwner,
multiple data records of the League table have the id of the owner as value for this
column. For associations with a multiplicity of zero or one, a null value indicates that
there are no associations for the data record of interest.
Figure 10-19 Mapping of the LeagueOwner/League association as a buried association.
• Separate table. Many-to-many associations are implemented using a separate
two-column table with foreign keys for both classes of the association. We call
this the association table. Each row in the association table corresponds to a link
between two instances. For example, we map the many-to-many
Tournament/Player association to an association table with two columns: one for
the id of the Tournaments, the other for the id of the Players. If a player is part of
multiple tournaments, each player/tournament association will have a separate
data record. Similarly, if a tournament includes multiple players, each player will
have a separate data record. The association table in Figure 10-20 contains two
links representing the membership of “alice” and “john” in the “novice”
Tournament.
Figure 10-20 Mapping of the Tournament/Player association as a separate table.
Mapping inheritance relationships
• Relational databases do not directly support inheritance, but there are two main options
for mapping an inheritance relationship to a database schema. In the first option, called
vertical mapping, similar to a one-to-one association, each class is represented by a table
and uses a foreign key to link the subclass tables to the superclass table. In the second
option, called horizontal mapping, the attributes of the superclass are pushed down into
the subclasses, essentially duplicating columns in the tables corresponding to subclasses.
Vertical mapping
• Given an inheritance relationship, we map the superclass and subclasses to individual
tables. The superclass table includes a column for each attribute defined in the superclass.
The superclass includes an additional column denoting the subclass that corresponds to
the data record. The subclass tables include a column for each attribute defined in the
superclass. All tables share the same primary key, that is, the identifier of the object. Data
records in the superclass and subclass tables with the same primary key value refer to the
same object.
• For example, in Figure 10-21, the classes User, LeagueOwner, and Player are each
mapped to a table with columns for the attributes of the classes. The User table
includes an additional column, role, which denotes the class of the data record.
The three tables share the same primary key, that is, if data records have the same
id value in each table, they correspond to the same object. The User Zoe, for
example, is a LeagueOwner, who can lead at most 12 Leagues. Zoe’s name is
stored in the User table; the maximum number of leagues is stored in the
LeagueOwner table. Similarly, the User John is a Player who has 126 credits (i.e.,
the number of Matches that John can play before renewing his membership).
John’s name is stored in the User table; his number of credits is stored in the
Player table.
Figure 10-21 Realizing the User inheritance hierarchy with a separate table.
• In general, to retrieve an object from the database, we need to examine first the data
record in the superclass table. This record includes the attribute values for the superclass
(e.g., “zoe”, the name of the user) and the subclass of the instance. We use the same object
id (e.g., “56”) to query the subclass indicated by the role attribute (i.e., “LeagueOwner”),
and retrieve the remainder of the attribute values (e.g., “maxNumLeagues = 12”). In case
of several levels of inheritance, we repeat the same procedure and reconstruct the object
with individual queries to each table.
Horizontal mapping
• Another way to realize inheritance is to push the attributes of the superclass down into the
subclasses, effectively removing the need for a superclass table. In this case, each
subclass table duplicates the columns of the superclass.
• For example, in the case of the User inheritance hierarchy, we create a table for
LeagueOwners and a table for Players (Figure 10-22). Each table includes a column for
its own attributes and for the attributes of the User class. In this case, we need a single
query to retrieve all attribute values for a single object.
Figure 10-22 Realizing the User inheritance hierarchy by duplicating columns.
• The trade-off between using a separate table for superclasses and duplicating
columns in the subclass tables is between modifiability and response time. If we
use a separate table, we can add attributes to the superclass simply by adding a
column to the superclass table. When adding a subclass, we add a table for the
subclass with a column for each attribute in the subclass. If we duplicate columns,
modifying the database schema is more complex and error prone. The advantage
of duplicating columns is that individual objects are not fragmented across a
number of tables, which results in faster queries. For deep inheritance hierarchies,
this can represent a significant performance difference.
• In general, we need to examine the likelihood of changes against the performance
requirements in the specific context of the application.
Testing
• Testing is the process of finding differences between the expected behavior specified
by system models and the observed behavior of the implemented system.
• Unit testing finds differences between a specification of an object and its realization
as a component.
• Structural testing finds differences between the system design model and a subset of
integrated subsystems.
• Functional testing finds differences between the use case model and the system.
• Performance testing finds differences between nonfunctional requirements and
actual system performance.
• When differences are found, developers identify the defect causing the observed failure
and modify the system to correct it. In other cases, the system model is identified as the
cause of the difference, and the system model is updated to reflect the system.
An Overview of Testing
• Reliability is a measure of success with which the observed behavior of a system
conforms to the specification of its behavior.
• Software reliability is the probability that a software system will not cause system failure
for a specified time under specified conditions [IEEE Std. 982.2- 1988].
• Failure is any deviation of the observed behavior from the specified behavior.
• An erroneous state (also called an error) means the system is in a state such that further
processing by the system will lead to a failure, which then causes the system to deviate
from its intended behavior.
• A fault, also called “defect” or “bug,” is the mechanical or algorithmic cause of an
erroneous state. The goal of testing is to maximize the number of discovered faults, which
then allows developers to correct them and increase the reliability of the system.
• We define testing as the systematic attempt to find faults in a planned way in the
implemented software. Contrast this definition with another common one: “testing is the
process of demonstrating that faults are not present.” The distinction between these two
definitions is important. Our definition does not mean that we simply demonstrate that the
program does what it is intended to do. The explicit goal of testing is to demonstrate the
presence of faults and nonoptimal behavior. Our definition implies that the developers are
willing to dismantle things. Moreover, for the most part, demonstrating that faults are not
present is not possible in systems of any realistic size.
• Most activities of the development process are constructive: during analysis, design, and
implementation, objects and relationships are identified, refined, and mapped onto a
computer environment. Testing requires a different thinking, in that developers try to
detect faults in the system, that is, differences between the reality of the system and the
requirements. Many developers find this difficult to do. One reason is the way we use the
word “success” during testing. Many project managers call a test case “successful” if it
does not find a fault; that is, they use the second definition of testing during development.
However, because “successful” denotes an achievement, and “unsuccessful” means
something undesirable, these words should not be used in this fashion during testing.
There are many techniques for increasing the reliability of a software system:
• Fault avoidance techniques try to detect faults statically, that is, without relying on the
execution of any of the system models, in particular the code model. Fault avoidance tries
to prevent the insertion of faults into the system before it is released. Fault avoidance
includes development methodologies, configuration management, and verification.
• Fault detection techniques, such as debugging and testing, are uncontrolled and
controlled experiments, respectively, used during the development process to identify
erroneous states and find the underlying faults before releasing the system. Fault detection
techniques assist in finding faults in systems, but do not try to recover from the failures
caused by them. In general, fault detection techniques are applied during development, but
in some cases they are also used after the release of the system. The blackboxes in an
airplane to log the last few minutes of a flight is an example of a faul detection technique.
• Fault tolerance techniques assume that a system can be released with faults and that
system failures can be dealt with by recovering from them at runtime. For example,
modular redundant systems assign more than one component with the same task, then
compare the results from the redundant components. The space shuttle has five onboard
computers running two different pieces of software to accomplish the same task.
• In this chapter, we focus on fault detection techniques, including reviews and testing.
A review is the manual inspection of parts or all aspects of the system without actually
executing the system. There are two types of reviews: walkthrough and inspection. In
a code walkthrough, the developer informally presents the API (Application
Programmer Interface), the code, and associated documentation of the component to
the review team. The review team makes comments on the mapping of the analysis
and object design to the code using use cases and scenarios from the analysis phase.
An inspection is similar to a walkthrough, but the presentation of the component is
formal. In fact, in a code inspection, the developer is not allowed to present the
artifacts (models, code, and documentation). This is done by the review team, which is
responsible for checking the interface and code of the component against the
requirements. It also checks the algorithms for efficiency with respect to the
nonfunctional requirements. Finally, it checks comments about the code and compares
them with the code itself to find inaccurate and incomplete comments. The developer
is only present in case the review needs clarifications about the definition and use of
data structures or algorithms. Code reviews have proven to be effective at detecting
faults. In some experiments, up to 85 percent of all identified faults were found in
code reviews [Fagan, 1976], [Jones, 1977], [Porter et al., 1997].
• Debugging assumes that faults can be found by starting from an unplanned
failure. The developer moves the system through a succession of states, ultimately
arriving at and identifying the erroneous state. Once this state has been identified,
the algorithmic or mechanical fault causing this state must be determined. There
are two types of debugging: The goal of correctness debugging is to find any
deviation between observed and specified functional requirements. Performance
debugging addresses the deviation between observed and specified nonfunctional
requirements, such as response time.
• Testing is a fault detection technique that tries to create failures or erroneous
states in a planned way. This allows the developer to detect failures in the system
before it is released to the customer. Note that this definition of testing implies that
a successful test is a test that identifies faults. We will use this definition
throughout the development phases. Another often-used definition of testing is
that “it demonstrates that faults are not present.” We will use this definition only
after the development of the system when we try to demonstrate that the delivered
system fulfills the functional and nonfunctional requirements.
Figure 11-1 depicts an overview of testing activities:
• Test planning allocates resources and schedules the testing. This activity should
occur early in the development phase so that sufficient time and skill is dedicated to
testing. For example, developers can design test cases as soon as the models they
validate become stable.
• Usability testing tries to find faults in the user interface design of the system.
Often, systems fail to accomplish their intended purpose simply because their users
are confused by the user interface and unwillingly introduce erroneous data.
• Unit testing tries to find faults in participating objects and/or subsystems with
respect to the use cases from the use case model.
• Integration testing is the activity of finding faults by testing individual
components in combination. Structural testing is the culmination of integration
testing involving all components of the system. Integration tests and structural tests
exploit knowledge from the SDD (System Design Document) using an integration
strategy described in the Test Plan (TP).
• System testing tests all the components together, seen as a single system to
identify faults with respect to the scenarios from the problem statement and the
requirements and design goals identified in the analysis and system design,
respectively:
1. Functional testing tests the requirements from the RAD and the user manual.
2. Performance testing checks the nonfunctional requirements and additional
design goals from the SDD. Functional and performance testing are done by
developers.
3. Acceptance testing and installation testing check the system against the
project agreement and is done by the client, if necessary, with help by the
developers.
Figure 11-1 Testing activities and their related work products
(UML activity diagram). Swimlanes indicate who executes the
test.
Testing Concepts
In this section, we present the model elements used during testing (Figure 11-2):
• A test component is a part of the system that can be isolated for testing. A
component can be an object, a group of objects, or one or more subsystems.
• A fault, also called bug or defect, is a design or coding mistake that may cause
abnormal component behavior.
• An erroneous state is a manifestation of a fault during the execution of the
system. An erroneous state is caused by one or more faults and can lead to a
failure.
• A failure is a deviation between the specification and the actual behavior. A
failure is triggered by one or more erroneous states. Not all erroneous states
trigger a failure.
• A test case is a set of inputs and expected results that exercises a test component
with the purpose of causing failures and detecting faults.
• A test stub is a partial implementation of components on which the tested component
depends.
• A test driver is a partial implementation of a component that depends on the test
component. Test stubs and drivers enable components to be isolated from the rest of the
system for testing.
• A correction is a change to a component. The purpose of a correction is to
repair a fault. Note that a correction can introduce new faults

Figure 11-2 Model elements used during testing (UML class diagram).
Faults, Erroneous States, and Failures
• With the initial understanding of the terms from the definitions in Section 11.3,
let’s take a look at Figure 11-3. What do you see? Figure 11-3 shows a pair of
tracks that are not aligned with each other. If we envision a train running over the
tracks, it would crash (fail). However, the figure actually does not present a
failure, nor an erroneous state, nor a fault. It does not show a failure, because the
expected behavior has not been specified, nor is there any observed behavior.
Figure 11-3 also does not show an erroneous state, because that would mean that
the system is in a state that further processing will lead to a failure. We only see
tracks here; no moving train is shown. To speak about erroneous state, failure, or
fault, we need to compare the desired behavior (described in the use case in the
RAD) with the observed behavior (described by the test case). Assume that we
have a use case with a train moving from the upper left track to the lower right
track (Figure 11-4).
Figure 11-4 Use case DriveTrain specifying the expected
Figure 11-3 An example of a fault. The behavior of the train.
desired behavior is that the train remain
on the tracks.

We can then proceed to derive a test case that moves the train from the state
described in the entry condition of the use case to a state where it will crash,
namely when it is leaving the upper track (Figure 11-5).
Figure 11-5 Test case DriveTrain for the use case described in Figure 11-4.
• In other words, when executing this test case, we can demonstrate that the system
contains a fault. Note that the current state shown in Figure 11-6 is erroneous, but
does not show a failure.
• The misalignment of the tracks can be a result of bad communication between the
development teams (each track had to be positioned by one team) or because of a
wrong implementation of the specification by one of the teams (Figure 11-7). Both
of these are examples of algorithmic faults. You are probably already familiar with
many other algorithmic faults that are introduced during the implementation
phase. For example, “Exiting a loop too soon,” “exiting a loop too late,” “testing
for the wrong condition,” “forgetting to initialize a variable” are all
implementation-specific algorithmic faults. Algorithmic faults can also occur
during analysis and system design. Stress and overload problems, for example, are
object design specific algorithmic faults that lead to failure when data structures
are filled beyond their specified capacity. Throughput and performance failures are
possible when a system does not perform at the speed specified by the
nonfunctional requirements.
Figure 11-6 An example of an Figure 11-7 A fault can have an algorithmic cause.
erroneous state. Figure 11-8 A fault can have a mechanical
cause, such as an earthquake.
• A fault in the virtual machine of a software system is another example of a
mechanical fault: even if the developers have implemented correctly, that is, they
have mapped the object model correctly onto the code, the observed behavior can
still deviate from the specified behavior. In concurrent engineering projects, for
example, where hardware is developed in parallel with software, we cannot
always make the assumption that the virtual machine executes as specified. Other
examples of mechanical faults are power failures. Note the relativity of the terms
“fault” and “failure” with respect to a particular system component: the failure in
one system component (the power system) is the mechanical fault that can lead to
failure in another system component (the software system).
Test Cases
• A test case is a set of input data and expected results that exercises a
component with the purpose of causing failures and detecting faults. A test
case has five attributes: name, location, input, oracle, and log (Table 11-1). The
name of the test case allows the tester to distinguish between different test cases. A
heuristic for naming test cases is to derive the name from the requirement it is
testing or from the component being tested. For example, if you are testing a use
case Deposit(), you might want to call the test case Test_Deposit. If a test case
involves two components A and B, a good name would be Test_AB. The location
attribute describes where the test case can be found. It should be either the path
name or the URL to the executable of the test program and its inputs.
Table 11-1 Attributes of the class TestCase.
• Input describes the set of input data or commands to be entered by the actor of the
test case (which can be the tester or a test driver). The expected behavior of the
test case is the sequence of output data or commands that a correct execution of
the test should yield. The expected behavior is described by the oracle attribute.
The log is a set of time-stamped correlations of the observed behavior with the
expected behavior for various test runs.
• Once test cases are identified and described, relationships among test cases are
identified. Aggregation and the precede associations are used to describe the
relationships between the test cases. Aggregation is used when a test case can be
decomposed into a set of subtests. Two test cases are related via the precede
association when one test case must precede another test case.
• Figure 11-9 shows a test model where TestA must precede TestB and TestC. For
example, TestA consists of TestA1 and TestA2, meaning that once TestA1 and TestA2
are tested, TestA is tested; there is no separate test for TestA. A good test model has as
few associations as possible, because tests that are not associated with each other can be
executed independently from each other. This allows a tester to speed up testing, if the
necessary testing resources are available. In Figure 11-9, TestB and TestC can be tested
in parallel, because there is no relation between them.
• Test cases are classified into blackbox tests and whitebox tests, depending on which
aspect of the system model is tested. Blackbox tests focus on the input/output behavior
of the component. Blackbox tests do not deal with the internal aspects of the
component, nor with the behavior or the structure of the components. Whitebox tests
focus on the internal structure of the component. A whitebox test makes sure that,
independently from the particular input/output behavior, every state in the dynamic
model of the object and every interaction among the objects is tested. As a result,
whitebox testing goes beyond blackbox testing. In fact, most of the whitebox tests
require input data that could not be derived from a description of the functional
requirements alone. Unit testing combines both testing techniques: blackbox testing to
test the functionality of the component, and whitebox testing to test structural and
dynamic aspects of the component.
Figure 11-9 Test model with test cases. TestA consists of two tests, TestA1 and TestA2. TestB and
TestC can be tested independently, but only after TestA has been performed.
Test Stubs and Drivers
• Executing test cases on single components or combinations of components requires
the tested component to be isolated from the rest of the system. Test drivers and test
stubs are used to substitute for missing parts of the system. A test driver simulates
the part of the system that calls the component under test. A test driver passes the test
inputs identified in the test case analysis to the component and displays the results.
• A test stub simulates a component that is called by the tested component. The test
stub must provide the same API as the method of the simulated component and must
return a value compliant with the return result type of the method’s type signature.
Note that the interface of all components must be baselined. If the interface of a
component changes, the corresponding test drivers and stubs must change as well.
• The implementation of test stubs is a nontrivial task. It is not sufficient to write a test
stub that simply prints a message stating that the test stub was called. In most
situations, when component A calls component B, A is expecting B to perform some
work, which is then returned as a set of result parameters. If the test stub does not
simulate this behavior, A will fail, not because of a fault in A, but because the test
stub does not simulate B correctly.
• Even providing a return value is not always sufficient. For example, if a test stub
always returns the same value, it might not return the value expected by the calling
component in a particular scenario. This can produce confusing results and even
lead to the failure of the calling component, even though it is correctly
implemented. Often, there is a trade-off between implementing accurate test stubs
and substituting the test stubs by the actual component. For many components,
drivers and stubs are often written after the component is completed, and for
components that are behind schedule, stubs are often not written at all.
• To ensure that stubs and drivers are developed and available when needed, several
development methods stipulate that drivers be developed for every component.
This results in lower effort because it provides developers the opportunity to find
problems with the interface specification of the component under test before it is
completely implemented.
Corrections
Once tests have been executed and failures have been detected, developers change
the component to eliminate the suspected faults. A correction is a change to a
component whose purpose is to repair a fault. Corrections can range from a simple
modification to a single component, to a complete redesign of a data structure or a
subsystem. In all cases, the likelihood that the developer introduces new faults into
the revised component is high. Several techniques can be used to minimize the
occurrence of such faults:
• Problem tracking includes the documentation of each failure, erroneous state,
and fault detected, its correction, and the revisions of the components involved in
the change. Together with configuration management, problem tracking enables
developers to narrow the search for new faults
• Regression testing includes the reexecution of all prior tests after a change. This
ensures that functionality which worked before the correction has not been
affected. Regression testing is important in object-oriented methods, which call for
an iterative development process. This requires testing to be initiated earlier and
for test suites to be maintained after each iteration. Regression testing
unfortunately is costly, especially when part of the tests is not automated.
• Rationale maintenance includes the documentation of the rationale for the
change and its relationship with the rationale of the revised component. Rationale
maintenance enables developers to avoid introducing new faults by inspecting the
assumptions that were used to build the component.
Testing Activities
• Component inspection, which finds faults in an individual component through
the manual inspection of its source code
• Usability testing, which finds differences between what the system does and the
users’ expectation of what it should do
• Unit testing, which finds faults by isolating an individual component using test
stubs and drivers and by exercising the component using test cases
• Integration testing, which finds faults by integrating several components
together
• System testing, which focuses on the complete system, its functional and
nonfunctional requirements, and its target environment
Component Inspection
Inspections find faults in a component by reviewing its source code in a formal
meeting. Inspections can be conducted before or after the unit test. The first
structured inspection process was Michael Fagan’s inspection method [Fagan,
1976]. The inspection is conducted by a team of developers, including the author of
the component, a moderator who facilitates the process, and one or more reviewers
who find faults in the component. Fagan’s inspection method consists of five steps:
• Overview. The author of the component briefly presents the purpose and scope
of the component and the goals of the inspection.
• Preparation. The reviewers become familiar with the implementation of the
component.
• Inspection meeting. A reader paraphrases the source code of the component, and
the inspection team raises issues with the component. A moderator keeps the
meeting on track.
• Rework. The author revises the component.
• Follow-up. The moderator checks the quality of the rework and may determine
the component that needs to be reinspected.
• The critical steps in this process are the preparation phase and the inspection
meeting. During the preparation phase, the reviewers become familiar with the
source code; they do not yet focus on finding faults. During the inspection
meeting, the reader paraphrases the source code, that is, he reads each source code
statement and explains what the statement should do. The reviewers then raise
issues if they think there is a fault. Most of the time is spent debating whether or
not a fault is present, but solutions to repair the fault are not explored at this point.
During the overview phase of the inspection, the author states the objectives of the
inspection. In addition to finding faults, reviewers may also be asked to look for
deviations from coding standards or for inefficiencies.
Usability Testing
• Usability testing tests the user’s understanding of the system. Usability testing
does not compare the system against a specification. Instead, it focuses on finding
differences between the system and the users’ expectation of what it should do. As
it is difficult to define a formal model of the user against which to test, usability
testing takes an empirical approach: participants representative of the user
population find problems by manipulating the user interface or a simulation
thereof. Usability tests are also concerned with user interface details, such as the
look and feel of the user interface, the geometrical layout of the screens, sequence
of interactions, and the hardware. For example, in case of a wearable computer, a
usability test would test the ability of the user to issue commands to the system
while lying in an awkward position, as in the case of a mechanic looking at a
screen under a car while checking a muffler.
• The technique for conducting usability tests is based on the classical approach for
conducting a controlled experiment. Developers first formulate a set of test
objectives, describing what they hope to learn in the test. These can include, for
example, evaluating specific dimensions or geometrical layout of the user
interface, evaluating the impact of response time on user efficiency, or evaluating
whether the online help documentation is sufficient for novice users. The test
objectives are then evaluated in a series of experiments in which participants are
trained to accomplish predefined tasks (e.g., exercising the user interface feature
under investigation). Developers observe the participants and collect data
measuring user performance (e.g., time to accomplish a task, error rate) and
preferences (e.g, opinions and thought processes) to identify specific problems
with the system or collect ideas for improving it [Rubin, 1994].
• There are two important differences between controlled experiments and
usability tests. Whereas the classical experimental method is designed to refute a
hypothesis, the goal of usability tests is to obtain qualitative information on how to
fix usability problems and how to improve the system. The other difference is the
rigor with which the experiments are performed. It has been shown that even a
series of quick focused tests starting as early as requirements elicitation is
extremely helpful. Nielsen uses the term discount usability engineering to refer to
simplified usability tests that can be accomplished at a fraction of the time and
cost of a fullblown study, noting that a few usability tests are better than none at
all [Nielsen & Mack, 1994]. Examples of discount usability tests include using
paper scenario mock-ups (as opposed to a videotaped scenario), relying on
handwritten notes as opposed to analyzing audio tape transcripts, or using fewer
subjects to elicit suggestions and uncover major defects (as opposed to achieving
statistical significance and using quantitative measures).
There are three types of usability tests:
• Scenario test. During this test, one or more users are presented with a
visionary scenario of the system. Developers identify how quickly users are able
to understand the scenario, how accurately it represents their model of work, and
how positively they react to the description of the new system. The selected
scenarios should be as realistic and detailed as possible. A scenario test allows
rapid and frequent feedback from the user. Scenario tests can be realized as
paper mock-ups3 or with a simple prototyping environment, which is often easier
to learn than the programming environment used for development. The advantage
of scenario tests is that they are cheap to realize and to repeat. The disadvantages
are that the user cannot interact directly with the system and that the data are
fixed.
• Prototype test. During this type of test, the end users are presented with a piece of
software that implements key aspects of the system. A vertical prototype
completely implements a use case through the system. Vertical prototypes are
used to evaluate core requirements, for example, response time of the system or
user behavior under stress. A horizontal prototype implements a single layer in
the system; an example is a user interface prototype, which presents an interface
for most use cases (without providing much or any functionality). User interface
prototypes are used to evaluate issues such as alternative user interface concepts or
window layouts. A Wizard of Oz prototype is a user interface prototype in which
a human operator behind the scenes pulls the levers [Kelly, 1984]. Wizard of
Oz prototypes are used for testing natural language applications, when the speech
recognition or the natural language parsing subsystems are incomplete. A human
operator intercepts user queries and rephrases them in terms that the system
understands, without the test user being aware of the operator. The advantages of
prototype tests are that they provide a realistic view of the system to the user and
that prototypes can be instrumented to collect detailed data. However, prototypes
require more effort to build than test scenarios.
• Product test. This test is similar to the prototype test except that a functional
version of the system is used in place of the prototype. A product test can only be
conducted after most of the system is developed. It also requires that the system be
easily modifiable such that the results of the usability test can be taken into
account.
In all three types of tests, the basic elements of usability testing include [Rubin,
1994]
• development of test objectives
• a representative sample of end users
• the actual or simulated work environment
• controlled, extensive interrogation, and probing of the users by the person
performing the usability test
• collection and analysis of quantitative and qualitative results
• recommendations on how to improve the system.
Unit Testing
• Unit testing focuses on the building blocks of the software system, that is, objects and
subsystems. There are three motivations behind focusing on these building blocks. First,
unit testing reduces the complexity of overall test activities, allowing us to focus on
smaller units of the system. Second, unit testing makes it easier to pinpoint and correct
faults, given that few components are involved in the test. Third, unit testing allows
parallelism in the testing activities; that is, each component can be tested independently of
the others.
• The specific candidates for unit testing are chosen from the object model and the system
decomposition. In principle, all the objects developed during the development process
should be tested, which is often not feasible because of time and budget constraints. The
minimal set of objects to be tested should be the participating objects in use cases.
Subsystems should be tested as components only after each of the classes within that
subsystem have been tested individually.
• Existing subsystems, which were reused or purchased, should be treated as
components with unknown internal structure. This applies in particular, to
commercially available subsystems, where the internal structure is not known or
available to the developer.
• Equivalence testing
• This blackbox testing technique minimizes the number of test cases. The possible
inputs are partitioned into equivalence classes, and a test case is selected for
each class. The assumption of equivalence testing is that systems usually behave
in similar ways for all members of a class. To test the behavior associated with an
equivalence class, we only need to test one member of the class. Equivalence
testing consists of two steps: identification of the equivalence classes and selection
of the test inputs. The following criteria are used in determining the equivalence
classes.
• Coverage. Every possible input belongs to one of the equivalence classes.
• Disjointedness. No input belongs to more than one equivalence class.
• Representation. If the execution demonstrates an erroneous state when a
particular member of a equivalence class is used as input, then the same erroneous
state can be detected by using any other member of the class as input.

• For each equivalence class, at least two pieces of data are selected: a typical
input, which exercises the common case, and an invalid input, which exercises
the exception handling capabilities of the component. After all equivalence
classes have been identified, a test input for each class has to be identified that
covers the equivalence class. If there is a possibility that not all the elements of the
equivalence class are covered by the test input, the equivalence class must be split
into smaller equivalence classes, and test inputs must be identified for each of the
new classes.
• For example, consider a method that returns the number of days in a month, given
the month and year (see Figure 11-10). The month and year are specified as
integers. By convention, 1 represents the month of January, 2 the month of
February, and so on. The range of valid inputs for the year is 0 to maxInt.

Figure 11-10 Interface for a method computing the number of days in a given month (in Java). The
getNumDaysInMonth() method takes two parameters, a month and a year, both specified as
integers.
• We find three equivalence classes for the month parameter: months with 31 days
(i.e., 1, 3, 5, 7, 8, 10, 12), months with 30 days (i.e., 4, 6, 9, 11), and February,
which can have 28 or 29 days. Nonpositive integers and integers larger than 12 are
invalid values for the month parameter. Similarly, we find two equivalence classes
for the year: leap years and non–leap years. By specification, negative integers are
invalid values for the year.
• First we select one valid value for each equivalence class (e.g., February, June,
July, 1901, and 1904). Given that the return value of the getNumDaysInMonth()
method depends on both parameters, we combine these values to test for
interaction, resulting in the six equivalence classes displayed in Table 11-2.
• Boundary testing
• This special case of equivalence testing focuses on the conditions at the boundary
of the equivalence classes. Rather than selecting any element in the
equivalence class, boundary testing requires that the elements be selected
from the “edges” of the equivalence class. The assumption behind boundary
testing is that developers often overlook special cases at the boundary of the
equivalence classes (e.g., 0, empty strings, year 2000).
• In our example, the month of February presents several boundary cases. In
general, years that are multiples of 4 are leap years. Years that are multiples
of 100, however, are not leap years, unless they are also multiple of 400. For
example, 2000 was a leap year, whereas 1900 was not. Both year 1900 and
2000 are good boundary cases we should test. Other boundary cases include
the months 0 and 13, which are at the boundaries of the invalid equivalence
class. Table 11-3 displays the additional boundary cases we selected for the
getNumDaysInMonth() method.
A disadvantage of equivalence and boundary testing is that these techniques do not explore
combinations of test input data. In many cases, a program fails because a combination of
certain values causes the erroneous fault. Cause-effect testing addresses this problem by
establishing logical relationships between input and outputs or inputs and transformations.
The inputs are called causes, the outputs or transformations are effects. The technique is
based on the premise that the input/output behavior can be transformed into a Boolean
function. For details on this technique and another technique called “error guessing,” we
refer you to the literature on testing (for example [Myers, 1979]).
• Path testing
• This whitebox testing technique identifies faults in the implementation of the
component. The assumption behind path testing is that, by exercising all
possible paths through the code at least once, most faults will trigger failures.
The identification of paths requires knowledge of the source code and data
structures. The starting point for path testing is the flow graph. A flow graph
consists of nodes representing executable blocks and edges representing flow of
control. A flow graph is constructed from the code of a component by mapping
decision statements (e.g., if statements, while loops) to nodes. Statements between
each decision (e.g., then block, else block) are mapped to other nodes.
Associations between each node represent the precedence relationships. Figure 11-
11 depicts an example of a faulty implementation of the getNumDaysInMonth()
method. Figure 11-12 depicts the equivalent flow graph as a UML activity
diagram. In this example, we model decisions with UML branches, blocks with
UML actions, and control flow with UML transitions.
Figure 11-11 An example of a (faulty) implementation of the
getNumDaysInMonth() method (Java).
Figure 11-12 Equivalent flow graph for the (faulty) implementation of the
getNumDaysInMonth() method of Figure 11-11 (UML activity diagram).
• Complete path testing consists of designing test cases such that each edge in the
activity diagram is traversed at least once. This is done by examining the condition
associated with each branch point and selecting an input for the true branch and
another input for the false branch. For example, examining the first branch point in
Figure 11-12, we select two inputs: year=0 (such that year < 1 is true) and
year=1901 (such that year < 1 is false). We then repeat the process for the second
branch and select the inputs month=1 and month=2. The input (year=0, month=1)
produces the path {throw1}. The input (year=1901, month=1) produces a second
path {n=32 return}, which uncovers one of the faults in the
getNumDaysInMonth() method. By repeating this process for each node, we
generate the test cases depicted in Table 11-4.
• We can similarly construct the activity diagram for the method isLeapYear() and
derive test cases to exercise the single branch point of this method (Figure 11-13).
Note that the test case (year = 1901, month = 2) of the getNumDaysInMonth()
method already exercises one of the paths of the isLeapYear() method. By
systematically constructing tests to cover all the paths of all methods, we can deal
with the complexity associated with a large number of methods.
• Using graph theory, it can be shown that the minimum number of tests
necessary to cover all edges is equal to the number of independent paths
through the flow graph [McCabe, 1976]. This is defined as the cyclomatic
complexity CC of the flow graph, which is
CC = number of edges - number of nodes + 2
where the number of nodes is the number of branches and actions, and the number
of edges is the number of transitions in the activity diagram. The cyclomatic
complexity of the getNumDaysInMonth() method is 6, which is also the number of
test cases we found in Table 11-4. Similarly, the cyclomatic complexity of the
isLeapYear() method and the number of derived test cases is 2.
• By comparing the test cases we derived from the equivalence classes (Table 11-2)
and boundary cases (Table 11-3) with the test cases we derived from the flow
graph (Table 11-4 and Figure 11-13), several differences can be noted. In both
cases, we test the method extensively for computations involving the month of
February. However, because the implementation of isLeapYear() does not take into
account years divisible by 100, path testing did not generate any test case for this
equivalence class.
• In general, path testing and whitebox methods can detect only faults resulting from
exercising a path in the program, such as the faulty numDays=32 statement.
Whitebox testing methods cannot detect omissions, such as the failure to handle
the non–leap year 1900. Path testing is also heavily based on the control structure
of the program; faults associated with violating invariants of data structures, such
as accessing an array out of bounds, are not explicitly addressed. However, no
testing method short of exhaustive testing can guarantee the discovery of all faults.
In our example, neither equivalence testing or path testing uncovered the fault
associated with the month of August.
Figure 11-13 Equivalent flow graph for the (faulty) isLeapYear() method implementation of Figure 11-11 (UML
activity diagram) and derived tests. The test in italic is redundant with a test we derived for the
getNumDaysInMonth() method.
• State-based testing
• This testing technique was recently developed for object-oriented systems [Turner & Robson,
1993]. Most testing techniques focus on selecting a number of test inputs for a given state of the
system, exercising a component or a system, and comparing the observed outputs with an
oracle. State-based testing, however, compares the resulting state of the system with the
expected state. In the context of a class, state-based testing consists of deriving test cases from
the UML state machine diagram for the class. For each state, a representative set of stimuli is
derived for each transition (similar to equivalence testing). The attributes of the class are then
instrumented and tested after each stimuli has been applied to ensure that the class has reached
the specified state.
• For example, Figure 11-14 depicts a state machine diagram and its associated tests for the
2Bwatch. It specifies which stimuli change the watch from the high-level state MeasureTime to
the high-level state SetTime. It does not show the low-level states of the watch when the date
and time change, either because of actions of the user or because of time passing. The test
inputs in Figure 11-14 were generated such that each transition is traversed at least once. After
each input, instrumentation code checks if the watch is in the predicted state and reports a
failure otherwise. Note that some transitions (e.g., transition 3) are traversed several times, as it
is necessary to put the watch back into the SetTime state (e.g., to test transitions 4, 5, and 6).
Only the first eight stimuli are displayed. The test inputs for the DeadBattery state were not
generated.
Figure 11-14 UML state machine diagram and resulting tests for 2Bwatch SetTime use case. Only the first eight
stimuli are shown
• Currently, state-based testing presents several difficulties. Because the state of a class is
encapsulated, test cases must include sequences for putting classes in the desired state
before given transitions can be tested. State-based testing also requires the
instrumentation of class attributes. Although state-based testing is currently not part of the
state of the practice, it promises to become an effective testing technique for object-
oriented systems as soon as proper automation is provided.
• Polymorphism testing
• Polymorphism introduces a new challenge in testing because it enables messages to be
bound to different methods based on the class of the target. Although this enables
developers to reuse code across a larger number of classes, it also introduces more cases
to test. All possible bindings should be identified and tested [Binder, 2000].
• Consider the NetworkInterface Strategy design pattern, Object Design: Reusing Pattern
Solutions (see Figure 11-15). The Strategy design pattern uses polymorphism to shield the
context (i.e., the NetworkConnection class) from the concrete strategy (i.e., the Ethernet,
WaveLAN, and UMTS classes). For example, the [Link]() method
calls the [Link]() method to send bytes across the current
NetworkInterface, regardless of the actual concrete strategy. This means that, at run
time, the [Link]() method invocation can be bound to one of three
methods, [Link](), [Link](), [Link]().
Figure 11-15 A Strategy design pattern for encapsulating multiple implementations of a
NetworkInterface (UML class diagram).
• When applying the path testing technique to an operation that uses polymorphism,
we need to consider all dynamic bindings, one for each message that could be
sent. In [Link]() in the left column of Figure 11-16, we invoke the
[Link]() operation, which can be bound to either [Link](),
[Link](), or the [Link]() methods, depending on the class of the nif
object. To deal with this situation explicitly, we expand the original source code by
replacing each invocation of [Link]() with a nested if else
statement that tests for all subclasses ofNetworkInterface (right column of Figure
11-16). Depending on the class, nif is cast into the appropriate concrete class, and
the associated method is invoked.
• Note that in some situations, the number of paths can be reduced by eliminating
redundancy. For the sake of this example, we simply adopt a mechanical approach.
• Once the source code is expanded, we extract the flow graph (Figure 11-17) and
generate test cases covering all paths. This results in test cases that exercise the
send() method of all three concrete network interfaces.
• When many interfaces and abstract classes are involved, generating the flow graph
for a method of medium complexity can result in an explosion of paths. This
illustrates, on the one hand, how object-oriented code using polymorphism can
result in compact and extensible components, and on the other hand, how the
number of test cases increases when trying to achieve any acceptable path
coverage.
Figure 11-17 Equivalent flow graph for the expanded source code of the [Link]() method of Figure
11-16 (UML activity diagram).
Integration Testing
• Integration testing detects faults that have not been detected during unit testing by
focusing on small groups of components.
• Two or more components are integrated and tested, and when no new faults are
revealed, additional components are added to the group.
• If two components are tested together, we call this a double test. Testing three
components together is a triple test, and a test with four components is called a
quadruple test.
• There are two types of Integration testing
• horizontal integration testing strategies, in which components are integrated
according to layers.
• vertical integration testing strategies, in which components are integrated
according to functions.
• Horizontal integration testing strategies
• Several approaches have been devised to implement a horizontal integration testing
strategy: big bang testing, bottom-up testing, top-down testing, and sandwich
testing.
• Each of these strategies was originally devised by assuming that the system
decomposition is hierarchical and that each of the components belong to hierarchical
layers ordered with respect to the “Call” association.
• The big bang testing strategy assumes that all components are first tested
individually and then tested together as a single system.
• The bottom-up testing strategy first tests each component of the bottom layer
individually, and then integrates them with components of the next layer up.
• The top-down testing strategy unit tests the components of the top layer first, and
then integrates the components of the next layer down.
• The sandwich testing strategy combines the top-down and bottom-up strategies,
attempting to make use of the best of both
• Vertical integration testing strategies
• Vertical integration testing strategies focus on early integration. For a given use case,
the needed parts of each component, such the user interface, business logic,
middleware, and storage, are identified and developed in parallel and integration
tested.
• A system build with a vertical integration strategy produces release candidates.
• The drawback of vertical integration testing is that the system design is evolved
incrementally, often resulting in reopening major system design decisions.
System Testing
• System testing ensures that the complete system complies with the functional and
nonfunctional requirements.
• During system testing, several activities are performed:
• Functional testing. Test of functional requirements (from RAD)
• Performance testing. Test of nonfunctional requirements (from SDD)
• Pilot testing. Tests of common functionality among a selected group of end
users in the target environment
• Acceptance testing. Usability, functional, and performance tests performed
by the customer in the development environment against acceptance criteria
(from Project Agreement)
• Installation testing. Usability, functional, and performance tests performed by
the customer in the target environment. If the system is only installed at a
small selected set of customers it is called a beta test
Functional testing
• Functional testing, also called requirements testing, finds differences between the
functional requirements and the system.
• Functional testing is a blackbox technique: test cases are derived from the use case
model. In systems with complex functional requirements, it is usually not possible
to test all use cases for all valid and invalid inputs.
• The goal of the tester is to select those tests that are relevant to the user and have a
high probability of uncovering a failure.

Performance testing
• Performance testing finds differences between the design goals selected during
system design and the system. Because the design goals are derived from the
nonfunctional requirements, the test cases can be derived from the SDD or from
the RAD.
• The following tests are performed during performance testing:
• Stress testing checks if the system can respond to many simultaneous requests. For
example, if an information system for car dealers is required to interface with 6000
dealers, the stress test evaluates how the system performs with more than 6000
simultaneous users.
• Volume testing attempts to find faults associated with large amounts of data, such as
static limits imposed by the data structure, or high-complexity algorithms, or high disk
fragmentation.
• Security testing attempts to find security faults in the system. There are few systematic
methods for finding security faults. Usually this test is accomplished by “tiger teams”
who attempt to break into the system, using their experience and knowledge of typical
security flaws.
• Timing testing attempts to find behaviors that violate timing constraints described by the
nonfunctional requirements.
• Recovery tests evaluates the ability of the system to recover from erroneous states, such
as the unavailability of resources, a hardware failure, or a network failure.
• After all the functional and performance tests have been performed, and no failures have been
detected during these tests, the system is said to be validated.
Pilot testing
• During the pilot test, also called the field test, the system is installed and used by a
selected set of users. Users exercise the system as if it had been permanently
installed. No explicit guidelines or test scenarios are given to the users.
• Pilot tests are useful when a system is built without a specific set of requirements
or without a specific customer in mind. In this case, a group of people is invited to
use the system for a limited time and to give their feedback to the developers.
• An alpha test is a pilot test with users exercising the system in the development
environment.
• In a beta test, the pilot test is performed by a limited number of end users in the
target environment
Acceptance testing
• There are three ways the client evaluates a system during acceptance testing.
• In a benchmark test, the client prepares a set of test cases that represent typical
conditions under which the system should operate. Benchmark tests can be performed
with actual users or by a special test team exercising the system functions, but it is
important that the testers be familiar with the functional and nonfunctional
requirements so they can evaluate the system.
• In competitor testing, the new system is tested against an existing system or
competitor product.
• In shadow testing, a form of comparison testing, the new and the legacy systems are
run in parallel, and their outputs are compared.
• After acceptance testing, the client reports to the project manager which
requirements are not satisfied. Acceptance testing also gives the opportunity for a
dialog between the developers and client about conditions that have changed and
which requirements must be added, modified, or deleted because of the changes.
Installation testing
• After the system is accepted, it is installed in the target environment.
• A good system testing plan allows the easy reconfiguration of the system from the
development environment to the target environment. The desired outcome of the
installation test is that the installed system correctly addresses all requirements.
• In most cases, the installation test repeats the test cases executed during function
and performance testing in the target environment. Some requirements cannot be
executed in the development environment because they require target-specific
resources. To test these requirements, additional test cases have to be designed and
performed as part of the installation test.
• Once the customer is satisfied with the results of the installation test, system
testing is complete, and the system is formally delivered and ready for operation.
Managing Testing
• Many testing activities occur near the end of the project, when resources are
running low and delivery pressure increases. Often, trade-offs lie between the
faults to be repaired before delivery and those that can be repaired in a subsequent
revision of the system. In the end, however, developers should detect and repair a
sufficient number of faults such that the system meets functional and
nonfunctional requirements to an extent acceptable to the client.
Planning Testing
• Developers can reduce the cost of testing and the elapsed time necessary for its
completion through careful planning. Two key elements are to start the selection
of test cases early and to parallelize tests.
• Developers responsible for testing can design test cases as soon as the models they
validate become stable. Functional tests can be developed when the use cases are
completed. Unit tests of subsystems can be developed when their interfaces is
defined. Similarly, test stubs and drivers can be developed when component
interfaces are stable. Developing tests early enables the execution of tests to start
as soon as components become available. Moreover, given that developing tests
requires a close examination of the models under validation, developers can find
faults in the models even before the system is constructed. Note, however, that
developing tests early on introduces a maintenance problem: test cases, drivers,
and stubs need to be updated whenever the system models change.
• The second key element in shortening testing time is to parallelize testing
activities. All component tests can be conducted in parallel; double tests for
components in which no faults were discovered can be initiated while other
components are repaired. For example, the quad test A-B-C-D in Figure 11-26 can
be performed as soon as double tests A-B, A-C, and A-D have not resulted in any
failures. These double tests, in turn, can be performed as soon as unit test A is
completed. The quad test A-B-C-D can be performed in parallel with the double
test D-G and the triple test B-E-F, even if tests E, F, or G uncover failures and
delay the rest of the tests.
• Testing represents a substantial part of the overall project resources. A typical
guideline for projects following a Unified Process life cycle is to allocate 25
percent of project resources to testing (see Section 15.4.2; [Royce, 1998]).
However, this number can go up depending onsafety and reliability requirements
on the system. Hence, it is critical that test planning start early, as early as the use
case model is stable.
Figure 11-26 Example of a PERT chart for a schedule of the sandwich tests shown in Figure 11-21.
The PERT chart notation is defined in Section 3.3.4.
Documenting Testing
Testing activities are documented in four types of documents, the Test Plan, the
Test Case Specifications, the Test Incident Reports, and the Test Summary
Report:
• The Test Plan focuses on the managerial aspects of testing. It documents the
scope, approach, resources, and schedule of testing activities. The requirements
and the components to be tested are identified in this document.
• Each test is documented by a Test Case Specification. This document contains
the inputs, drivers, stubs, and expected outputs of the tests, as well as the tasks
to be performed.
• Each execution of each test is documented by a Test Incident Report. The
actual results of the tests and differences from the expected output are
recorded.
• The Test Report Summary document lists all the failures discovered during the
tests that need to be investigated. From the Test Report Summary, the developers
analyze and prioritize each failure and plan for changes in the system and in the
models. These changes in turn can trigger new test cases and new test executions.

The Test Plan (TP) and the Test Case Specifications (TCS) are written early in the
process, as soon as the test planning and each test case are completed. These
documents are under configuration management and updated as the system models
change. Figure 11-27 is an outline for a Test Plan.
Figure 11-27 Outline of a Test Plan.
Documents described in this section are based on the IEEE 829 standard on testing
documentation.
• Section 1 of the test plan describes the objectives and extent of the tests. The goal
is to provide a framework that can be used by managers and testers to plan and
execute the necessary tests in a timely and cost-effective manner.
• Section 2 explains the relationship of the test plan to the other documents
produced during the development effort such as the RAD, SDD, and ODD (Object
Design Document). It explains how all the tests are related to the functional and
nonfunctional requirements, as well as to the system design stated in the respective
documents. If necessary, this section introduces a naming scheme to establish the
correspondence between requirements and tests.
• Section 3, focusing on the structural aspects of testing, provides an overview of
the system in terms of the components that are tested during the unit test. The
granularity of components and their dependencies are defined in this section.
• Section 4, focusing on the functional aspects of testing, identifies all features and
combinations of features to be tested. It also describes all those features that are
not to be tested and the reasons for not testing them.
• Section 5 specifies generic pass/fail criteria for the tests covered in this plan. They
are supplemented by pass/fail criteria in the test design specification. Note that
“fail” in the IEEE standard terminology means “successful test” in our
terminology.
• Section 6 describes the general approach to the testing process. It discusses the
reasons for the selected integration testing strategy. Different strategies are often
needed to test different parts of the system. A UML class diagram can be used to
illustrate the dependencies between the individual tests and their involvement in
the integration tests.
• Section 7 specifies the criteria for suspending the testing on the test items
associated with the plan. It also specifies the test activities that must be repeated
when testing is resumed.
• Section 8 identifies the resources that are needed for testing. This should include
the physical characteristics of the facilities, including the hardware, software,
special test tools, and other resources needed (office space, etc.) to support the
tests.
• Section 9, the core of the test plan, lists the test cases that are used during testing.
Each test case is described in detail in a separate Test Case Specification
document. Each execution of these tests will be documented in a Test Incident
Report document. We describe these documents in more details later in this
section.
• Section 10 of the test plan covers responsibilities, staffing and training needs, risks
and contingencies, and the test schedule.
• Figure 11-28 is an outline of a Test Case Specification.
Figure 11-28 Outline of a Test Specification.

• The Test Case Specification identifier is the name of the test case, used to distinguish
it from other test cases. Conventions such as naming the test cases from the features
or the component being tested allow developers to more easily refer to test cases.
Section 2 of the TCS lists the components under test and the features being
exercised. Section 3 lists the inputs required for the test cases. Section 4 lists the
expected output. This output is computed manually or with a competing system
(such as a legacy system being replaced). Section 5 lists the hardware and software
platform needed to execute the test, including any test drivers or stubs. Section 6 lists
any constraints needed to execute the test such as timing, load, or operator
intervention. Section 7 lists the dependencies with other test cases.
• The Test Incident Report lists the actual test results and the failures that were
experienced. The description of the results must include which features were
demonstrated and whether the features have been met. If a failure has been
experienced, the test incident report should contain sufficient information to allow
the failure to be reproduced. Failures from all Test Incident Reports are collected
and listed in the Test Summary Report and then further analyzed and prioritized by
the developers.
Assigning Responsibilities
• Testing requires developers to find faults in components of the system. This is best done
when the testing is performed by a developer who was not involved in the development of
the component under test, one who is less reticent to break the component being tested
and who is more likely to find ambiguities in the component specification.
• For stringent quality requirements, a separate team dedicated to quality control is solely
responsible for testing. The testing team is provided with the system models, the source
code, and the system for developing and executing test cases. Test Incident Reports and
Test Report Summaries are then sent back to the subsystem teams for analysis and
possible revision of the system. The revised system is then retested by the testing team,
not only to check if the original failures have been addressed, but also to ensure that no
new faults have been inserted in the system.
• For systems that do not have stringent quality requirements, subsystem teams can
double as a testing team for components developed by other subsystem teams. The
architecture team can define standards for test procedures, drivers, and stubs, and
can perform as the integration test team. The same test documents can be used for
communication among subsystem teams.
One of the main problems of usability tests is with enrolling participants. Several
obstacles are faced by project managers in selecting real end users [Grudin, 1990]:
• The project manager is usually afraid that users will bypass established
technical support organizations and call the developers directly, once they
know how to get to them. Once this line of communication is established,
developers might be sidetracked too often from doing their assigned jobs.
• Sales personnel do not want developers to talk to “their” clients. Sales people are
afraid that developers may offend the client or create dissatisfaction with the
current generation of products (which still must be sold).
• The end users do not have time.
• The end users dislike being studied. For example, an automotive mechanic might
think that an augmented reality system will put him out of work.

Debriefing the participants is the key to coming to understanding how to improve


the usability of the system being tested. Even though the usability test uncovers and
exposes problems, it is often the debriefing session that illustrates why these
problems have occurred in the first place. It is important to write recommendations
on how to improve the tested components as fast as possible after the usability test is
finished, so they can be used by the developers to implement any necessary changes
in the system models of the tested component.
Regression Testing
• Object-oriented development is an iterative process. Developers modify, integrate,
and retest components often, as new features are implemented or improved. When
modifying a component, developers design new unit tests exercising the new
feature under consideration. They may also retest the component by updating and
rerunning previous unit tests. Once the modified component passes the unit tests,
developers can be reasonably confident about the changes within the component.
However, they should not assume that the rest of the system will work with the
modified component, even if the system has previously been tested. The
modification can introduce side effects or reveal previously hidden faults in other
components. The changes can exercise different assumptions about the unchanged
components, leading to erroneous states. Integration tests that are rerun on the
system to produce such failures are called regression tests.
The most robust and straightforward technique for regression testing is to
accumulate all integration tests and rerun them whenever new components are
integrated into the system. This requires developers to keep all tests up-to-date, to
evolve them as the subsystem interfaces change, and to add new integration tests as
new services or new subsystems are added. As regression testing can become time
consuming, different techniques have been developed for selecting specific
regression tests. Such techniques include [Binder, 2000]:
• Retest dependent components. Components that depend on the modified
component are the most likely to fail in a regression test. Selecting these tests will
maximize the likelihood of finding faults when rerunning all tests is not feasible.
• Retest risky use cases. Often, ensuring that the most catastrophic faults are
identified is more critical than identifying the largest number of faults. By
focusing first on use cases that present the highest risk, developers can minimize
the likelihood of catastrophic failures.
• Retest frequent use cases. When users are exposed to successive releases of the
same system, they expect that features that worked before continue to work in the
new release. To maximize the likelihood of this perception, developers focus on
the use cases that are most often used by the users.
Automating Testing
• Manual testing involves a tester to feed predefined inputs into the system using the user
interface, a command line console, or a debugger. The tester then compares the outputs
generated by the system with the expected oracle. Manual testing can be costly and error
prone when many tests are involved or when the system generates a large volume of
outputs. When requirements change and the system evolves rapidly, testing should be
repeatable. This makes these drawbacks worse, as it is difficult to guarantee that the same
test is executed under the same conditions every time.
• The repeatability of test execution can be achieved with automation. Although all aspects
of testing can be automated (including test case and oracle generation), the main focus of
test automation has been on execution. For system tests, test cases are specified in terms
of the sequence and timing of inputs and an expected output trace. The test harness can
then execute a number of test cases and compare the system output with the expected
output trace. For unit and integration tests, developers specify a test as a test driver that
exercises one or more methods of the classes under tests.
• The benefit of automating test execution is that tests are repeatable. Once a fault is
corrected as a result of a failure, the test that uncovered the failure can be repeated to
ensure that the failure does not occur anymore. Moreover, other tests can be run to ensure
(to a limited extent) that no new faults have been introduced. Moreover, when tests are
repeated many times, for example, in the case of refactoring (see Section 10.3.2), the cost
of testing is decreased substantially. However, note that developing a test harness and test
cases is an investment. If tests are run only once or twice, manual testing may be a better
alternative.
• An example of an automated test infrastructure is JUnit, a framework for writing and
automating the execution of unit tests for Java classes [JUnit, 2009]. The JUnit test
framework is made out of a small number of tightly integrated classes (Figure 11-29).
Developers write new test cases by subclassing the TestCase class. The setUp() and
tearDown() methods of the concrete test case initialize and clean up the testing
environment, respectively. The runTest() method includes the actual test code that
exercises the class under test and compares the results with an expected condition. The
test success or failure is then recorded in an instance of TestResult. TestCases can be
organized into TestSuites, which will invoke sequentially each of its tests. TestSuites can
also be included in other TestSuites, thereby enabling developers to group unit tests into
increasingly larger test suites.
Figure 11-29 JUnit test framework (UML class diagram).
• Typically, when using JUnit, each TestCase instance exercises one method of the
class under test. To minimize the proliferation of TestCase classes, all test methods
exercising the same class (and requiring the same test environment initialized by
the setUp() method) are grouped in the same ConcreteTestCase class. The actual
method that is invoked by runTest() can then be configured when creating
instances of TestCases. This enables developers to organize and selectively invoke
large number of tests.
Documenting Architecture: Architectural views
• Logical view: A high-level representation of a system's functionality and how its
components interact. It's typically represented using UML diagrams such as class
diagrams, sequence diagrams, and activity diagrams.
• Deployment view: Shows the distribution of processing across a set of nodes in the
system, including the physical distribution of processes and threads. This view focuses
on aspects of the system that are important after the system has been tested and is
ready to go into live operation.
• Cloud security architecture: Describes the structures that protect the data, workloads,
containers, virtual machines and APIs within the cloud environment.
• Data architecture view: Addresses the concerns of database designers and database
administrators, and system engineers responsible for developing and integrating the
various database components of the system. Modern data architecture views data as a
shared asset and does not allow departmental data silos.
• Behavioral architecture view: An approach in architectural design and spatial
planning that takes into consideration how humans interact with their physical
environment from a behavioral perspective. A behavioral architecture model is an
arrangement of functions and their sub-functions as well as interfaces (inputs and
outputs).

You might also like