Software Testing and Analysis Process Principles and Techniques
T
–
T
–
BusAc
CP > CT2
T
–
T
–
–
–
–
–
–
–
–
–
–
–
–
–
–
–
–
–
–
T
T T
T
F
T
F
T
F
T
T2
SP
T2
SP
T2
SP
Constraints at-most-one(EduAc, BusAc)
at-most-one(YP YT2)
YP > YT2 ↠ YP > YT1
at-most-one(CP CT2)
CP > CT2 ↠ CP > CT1
at-most-one(SP T2)
SP > T2 ↠ SP > T1 Abbreviations EduAc
Educational account
Edu
Educational price
BusAc
Business account
ND
No discount
CP > CT1
Current purchase greater than threshold 1
T1
Tier 1
T2
Tier 2
SP
Special Price
YP > YT1 CP > CT2
Year cumulative purchase greater than threshold 1 Current purchase greater than threshold 2
YP > YT2
Year cumulative purchase greater than threshold 2
SP > Sc
Special Price better than scheduled price
SP > T1
Special Price better than tier 1
SP > T2 Special Price better than tier 2 Open table as spreadsheet Figure 14.4: A decision table for the functional specification of feature Pricing of the Chipmunk Web site of Figure 14.3. The informal specification of Figure 14.3 identifies three customer profiles: educational, business, and individual. Figure 14.4 has only rows Educational account (EduAc) and Business account (BusAc). The choice individual corresponds to the combination False, False for choices EduAc and BusAc, and is thus redundant. The informal specification of Figure 14.3 indicates different discount policies depending on the relation between the current purchase and two progressive thresholds for the current purchase and the yearly cumulative purchase. These cases correspond to rows 3 through 6 of Figure 14.4. Conditions on thresholds that do not correspond to individual rows in the table can be defined by suitable combinations of values for these rows. Finally, the informal specification of Figure 14.3 distinguishes the cases in which special offer prices do not exceed either the scheduled or the tier 1 or tier 2 prices. Rows 7 through 9 of the table, suitably combined, capture all possible cases of special prices without redundancy. Constraints formalize the compatibility relations among different basic conditions listed in the table. For example, a cumulative purchase exceeding threshold tier 2 also exceeds threshold tier 1. The basic condition adequacy criterion requires generation of a test case specification for each column in the table. Don’t care entries of the table can be filled out arbitrarily, as long as constraints are not violated. The compound condition adequacy criterion requires a test case specification for each combination of truth values of basic conditions. The compound condition adequacy criterion generates a number of cases exponential in the number of basic conditions (2n combinations for n conditions) and can thus be applied only to small sets of basic conditions. For the modified condition/decision adequacy criterion (MC/DC), each column in the table represents a test case specification. In addition, for each of the original columns, MC/DC generates new columns by modifying each of the cells containing True or False. If modifying a truth value in one column results in a test case specifi∆ modified condition/decision coverage cation consistent with an existing column (agreeing in all places where neither is don’t care), the two test cases are represented by one merged column, provided they can be merged without violating constraints.
The MC/DC criterion formalizes the intuitive idea that a thorough test suite would not only test positive combinations of values – combinations that lead to specified outputs – but also negative combinations of values – combinations that differ from the specified ones – thus, they should produce different outputs, in some cases among the specified ones, in some other cases leading to error conditions. Applying MC/DC to column 1 of Figure 14.4 generates two additional columns: one for Educational Account = False and Special Price better than scheduled price = False, and the other for Educational Account = True and Special Price better than scheduled price = True. Both columns are already in the table (columns 3 and 2, respectively) and thus need not be added. Similarly, from column 2, we generate two additional columns corresponding to Educational Account = False and Special Price better than scheduled price = True, and Educational Account = True and Special Price better than scheduled price = False, also already in the table. Generation of a new column for each possible variation of the Boolean values in the columns, varying exactly one value for each new column, produces 78 new columns, 21 of which can be merged with columns already in the table. Figure 14.5 shows a table obtained by suitably joining the generated columns with the existing ones. Many don’t care cells from the original table are assigned either True or False values, to allow merging of different columns or to obey constraints. The few don’t-care entries left can be set randomly to obtain a complete test case. EduAc
T
T
F
F
F
F
F
F
F
F
F
F
F
BusAc
F
F
F
F
F
F
F
F
T
T
T
T
T
CP > CT1
T
T
F
F
T
T
T
T
F
F
T
T
F
YP > YT1
F
–
F
–
–
F
T
T
F
F
F
F
T
CP > CT2
F
F
F
F
F
F
T
T
F
F
F
F
F
YP > YT2
–
–
–
–
–
–
–
–
–
–
–
–
F
SP > Sc
F
T
F
T
F
T
–
–
F
T
F
–
F
SP > T1
F
T
F
T
F
T
F
T
F
T
F
T
F
F
–
F
–
F
–
F
T
F
–
F
–
F
SP >
T2 Out Edu SP ND Open table as spreadsheet
SP
T1
SP
T2
SP
ND
SP
T1
SP
EduAc
F
F
F
F
F
T
T
T
T
F
–
BusAc
T
T
T
T
T
F
F
F
F
F
F
CP > CT1
T
T
T
F
F
F
F
T
–
–
F
YP > YT1
T
F
F
T
T
T
–
–
–
T
T
CP > CT2
F
T
T
F
F
F
F
T
T
F
F
YP > YT2
F
–
–
T
T
F
–
–
–
T
F
SP > Sc
T
–
T
–
T
F
T
–
–
–
–
SP > T1
T
F
T
F
T
F
–
–
T
T
T
SP > T2
T
F
T
F
T
F
F
F
T
T
–
T2
SP
Edu
SP
Edu
SP
SP
Out SP T2 SP Open table as spreadsheet
SP
Abbreviations EduAc
Educational account
Edu
Educational price
BusAc
Business account
ND
No discount
CP > CT1
Current purchase greater than threshold 1
T1
Tier 1
T2
Tier 2
SP
Special Price
YP > YT1 CP > CT2 YP > YT2
Year cumulative purchase greater than threshold 1 Current purchase greater than threshold 2 Year cumulative purchase greater than threshold
2
T1
SP > Sc
Special Price better than scheduled price
SP > T1
Special Price better than tier 1
SP > T2 Special Price better than tier 2 Open table as spreadsheet Figure 14.5: The set of test cases generated for feature Pricing of the Chipmunk Web site applying the modified adequacy criterion. There are many ways of merging columns that generate different tables. The table in Figure 14.5 may not be the optimal one – the one with the fewest columns. The objective in test design is not to find an optimal test suite, but rather to produce a cost effective test suite with an acceptable trade-off between the cost of generating and executing test cases and the effectiveness of the tests. The table in Figure 14.5 fixes the entries as required by the constraints, while the initial table in Figure 14.4 does not. Keeping constraints separate from the table corresponding to the initial specification increases the number of don’t care entries in the original table, which in turn increases the opportunity for merging columns when generating new cases with the MC/DC criterion. For example, if business account (BusAc) = False, the constraint at-mostone(EduAc, BusAc) can be satisfied by assigning either True or False to entry educational account. Fixing either choice prematurely may later make merging with a newly generated column impossible. [2]The set of columns sharing a label is therefore equivalent to a logical
expression in sum-of-productsform.
14.4 Deriving Test Cases from Control and Data Flow Graphs Functional specifications are seldom given as control or data flow graphs, but sometimes they describe a set of mutually dependent steps to be executed in a given (partial) order, and can thus be modeled with flow graphs. The specification in Figure 14.6 describes the Chipmunk functionality that prepares orders for shipping. The specification indicates a set of steps to check the validity of fields in the order form. Type and validity of some of the values depend on other fields in the form. For example, shipping methods are different for domestic and international customers, and payment methods depend on customer type. Process shipping order The Process shipping order function checks the validity of orders and prepares the receipt. A valid order contains the following data: cost of goods If the cost of goods is less than the minimum processable order (MinOrder), then the order is invalid. shipping address The address includes name, address, city, postal code, and country. preferred shipping method If the address is domestic, the shipping method must be either land freight,or expedited land freight,or overnight air.If the address is international, the shipping method must be either air freight or expedited air freight; a shipping cost is computed based on address and shipping method. type of customer A customer can be individual, business,or educational. preferred method of payment Individual customers can use only credit cards, while business and educational customers can choose between credit card and invoice. card information If the method of payment is credit card, fields credit card number, name on card, expiration date, and billing address, if different from shipping address, must be provided. If credit card information is not valid, the user can either provide new data or abort the order. The outputs of Process shipping order are validity Validity is a Boolean output that indicates whether the order can be processed. total charge The total charge is the sum of the value of goods and the computed
shipping costs (only if validity = true). payment status If all data are processed correctly and the credit card information is valid or the payment method is by invoice, payment status is set to valid, the order is entered, and a receipt is prepared; otherwise validity = false. Figure 14.6: Functional specification of the feature Process shipping order of the Chipmunk Web site. The informal specification in Figure 14.6 can be modeled with a control flow graph, where the nodes represent computations and branches represent control flow consistent with the dependencies among computations, as illustrated in Figure 14.7. Given a control or a data flow graph model, we can generate test case specifications using the criteria originally devised for structural testing and described in Chapters 12 and 13.
Figure 14.7: A control flow graph model corresponding to functionality Process shipping order of Figure 14.6. Control flow testing criteria require test cases that exercise all elements of a particular kind in a graph model. The node adequacy criterion requires each node to be exercised at least once, and corresponds to statement testing. It is easy to verify that ∆ node adequacy criterion test suite T-node in Figure 14.8, consisting of test case specifications TC-1 and TC-2, causes all nodes of the control flow graph of Figure 14.7 to be traversed, and thus Tnode satisfies the node adequacy criterion. T-node Open table as spreadsheet Case
Too
Ship
Ship
Cust
Pay
Same
CC
small
where
method
type
method
addr
valid
TC-1
No
Int
Air
Bus
CC
No
TC-2
No
Dom
Air
Ind
CC
–
Yes No (abort)
Abbreviations: Too small
CostOfGoods
Ship where
Shipping address, Int = international, Dom = domestic
Ship how
Air = air freight, Land = land freight
Cust type
Bus = business, Edu = educational, Ind = individual
Pay method
CC = credit card, Inv = invoice
Same addr
Billing address = shipping address ?
CC Valid
Credit card information passes validity check?
Figure 14.8: Test suite T-node, comprising test case specifications TC-1 and TC-2, exercises each of the nodes in a control flow graph model of the specification in Figure 14.6. The branch adequacy criterion requires each branch to be exercised at least once: each edge of the graph must be traversed by at least one test case. Test suite T-branch (Figure 14.9) covers all branches of the control flow graph of Figure 14.7 and thus satisfies the branch adequacy criterion. T-branch Open table as spreadsheet Case
Too
Ship
Ship
Cust
Pay
Same
CC
small
where
method
type
method
addr
valid
TC-1
No
Int
Air
Bus
CC
No
Yes
TC-2
No
Dom
Land
–
–
–
–
TC-3
Yes
–
–
–
–
–
–
TC-4
No
Dom
Air
–
–
–
–
–
–
–
–
–
Edu
Inv
–
–
–
–
–
CC
Yes
–
No
–
–
–
CC
–
No (abort)
No
–
–
–
CC
–
No (no abort)
TC-5
No
Int
TC-6
No
–
TC-7
No
TC-8 TC-9
Land
Abbreviations: Too small Ship where
CostOfGoods
Ship how
Air = air freight, Land = land freight
Cust type
Bus = business, Edu = educational, Ind = individual
Pay method
CC = credit card, Inv = invoice
Same addr
Billing address = shipping address ?
CC Valid
Credit card information passes validity check?
Figure 14.9: Test suite T-branch exercises each of the decision outcomes in a control flow graph model of the specification in Figure 14.6. In principle, other test adequacy criteria described in Chapters 12 and 13 can be applied to more complex control structures derived from specifications, such as loops. A good functional specification should rarely result in a complex control structure, but data flow testing may be useful at a much coarser structure (e.g., to test interaction of transactions through a database).
14.5 Deriving Test Cases from Grammars Functional specifications for complex documents or domain-specific notations, as well as for conventional compilers and interpreters, are often structured as an annotated grammar or set of regular expressions. Test suites can be systematically derived from these grammatical structures. The informal specification of the Chipmunk Web site advanced search, shown in Figure 14.10, defines the syntax of a search pattern. Not surprisingly, this specification can easily be expressed as a grammar. Figure 14.11 expresses the specification as a grammar in Backus Naur Form (BNF). Advanced search The Advanced search function allows for searching elements in the Web site database. The key for searching can be: a simple string, i.e., a simple sequence of characters a compound string, i.e., a string terminated with character *, used as wild character, or a string composed of substrings included in braces and separated with commas, used to indicate alternatives a combination of strings, i.e., a set of strings combined with the Boolean operators NOT, AND, OR, and grouped within parentheses to change the priority of operators. Examples: laptop The routine searches for string “laptop” {DVD*,CD*} The routine searches for strings that start with substring “DVD” or “CD” followed by any number of characters. NOT (C2021*) AND C20* The routine searches for strings that start with substring “C20” followed by any number of characters, except substring “21.” Figure 14.10: Functional specification of the feature Advanced search of the Chipmunk Web site.
Figure 14.11: BNF description of functionality Advanced search A second example is given in Figure 14.12, which specifies a product configuration of the Chipmunk Web site. In this case, the syntactic structure of product configuration is described by an XML schema, which defines an element Model of type ProductConfigurationType. XML schemata are essentially a variant of BNF, so it is not difficult to render the schema in the same BNF notation, as shown in Figure 14.11. 2 3 4 5 6 Chipmunk Computers – Product Configuration Schema 7 Copyright 2001 D. Seville, Chipmunk Computers Inc. 8 9 10 11 12 13 14 16 18 19 20 21 22 23 25 26 27 28 1
Figure 14.12: An XML schema description of a Product configuration on the Chipmuk Web site. Items are enclosed in matching tags (〈tag〉 text 〈/tag〉) or incorporated in
a self-terminating tag (〈tag attribute=”value” /〉). The schema describes type ProductConfigurationType as a tuple composed of a required field modelNumber of type string; a set (possibly empty) of Components, each of which is composed of two stringvalued fields ComponentType and ComponentValue; and a possibly empty set of OptionalComponents, each of which is composed of a single string-valued ComponentType. Grammars are well suited to represent inputs of varying and unbounded size, with recursive structures and boundary conditions. These characteristics are not easily addressed with the fixed lists of parameters required by conventional combinatoric techniques described in Chapter 11, or by other model-based techniques presented in this chapter. Generating test cases from grammar specifications is straightforward and can easily be automated. Each test case is a string generated from the grammar. To produce a string, we start from a non terminal symbol and progressively apply productions to substitute substrings for non terminals occurring in the current string, until we obtain a string composed only of terminal symbols. In general, we must choose among several applicable production rules at each step. A simple criterion requires each production to be exercised at least once in producing a set of test cases. The number and complexity of the generated test cases depend on the order of application of the productions. If we first apply productions with non terminals on the right-hand side, we generate a smaller set of large test cases. First applying productions with only terminals on the right-hand side generates larger sets of smaller test cases. An algorithm that favors non terminals applied to the BNF for advanced search of Figure 14.10, exercises all the productions to generate the single test case not Char {*, Char} and (Char or Char) The derivation tree for this test case is given in Figure 14.14. It shows that each production of the BNF is exercised at least once.
Figure 14.14: The derivation tree of a test case for functionality Advanced Search derived from the BNF specification of Figure 14.11. The simple production coverage criterion is subsumed by a richer criterion that applies boundary conditions on the number of times each recursive production is applied successively. To generate test cases for boundary conditions we need to choose a minimum and maximum number of applications of each recursive production and then generate a test case for the minimum, maximum, one greater than minimum and one smaller than maximum. The approach is essentially similar to boundary interior path testing of program loops (see Section 12.5 of Chapter 12, page 222), where the “loop” in this case is in repeated applications of a production. To apply the boundary condition criterion, we need to annotate recursive productions with limits. Names and limits are shown in Figure 14.15, which extends the grammar of Figure 14.13. Alternatives within compound productions are broken out into individual productions. Production names are added for reference, and limits are added to recursive productions. In the example of Figure 14.15, the limit of productions compSeq1 and optCompSeq1 is set to 16; we assume that each model can have at most 16 required and 16 optional components.
Figure 14.13: BNF description of Product configuration.
Figure 14.15: The BNF description of Product Configuration extended with production names and limits. The boundary condition grammar-based criterion would extend the minimal set by adding test cases that cover the following choices: zero required components (compSeq1 applied 0 times) one required component (compSeq1 applied 1 time) fifteen required components (compSeq1 applied n – 1 times) sixteen required components (compSeq1 applied n times) zero optional components (optCompSeq1 applied 0 times) one optional component (optCompSeq1 applied 1 time) fifteen optional components (optCompSeq1 applied n – 1 times) sixteen optional components (optCompSeq1 applied n times) Probabilistic grammar-based criteria assign probabilities to productions, indicating which production to select at each step to generate test cases. Unlike names and limits, probabilities are attached to grammar productions as a separate set of annotations. We can generate several sets of test cases from the same grammar with different sets of probabilities, called “seeds.” Figure 14.16 shows a sample seed for the grammar that specifies the product configuration functionality of the Chipmunk Web site presented in Figure 14.15. weight
Model
1
weight
compSeq1
10
weight
compSeq2
0
weight
optCompSeq1
10
weight
optCompSeq2
0
weight
Comp
1
weight
OptComp
1
weight
modNum
1
weight
CompTyp
1
weight
CompVal
1
Figure 14.16: Sample seed probabilities for BNF productions of Product configuration. Probabilities are interpreted as weights that determine how frequently each production is used to generate a test case. The equal weight for compSeq1 and optCompSeq1 in Figure 14.16 indicates that test cases are generated by balancing use of these two productions; they contain approximately the same number of required and optional components. Weight 0 disables the productions, which are then applied only when application of competing productions reaches the limit indicated in the grammar.
Open Research Issues As long as there have been models of software, there has been model-based testing. A recent and ongoing ferment of activity in model-based testing is partly the result of wider use of models throughout software development. Ongoing research will certainly include test design based on software architecture, domain-specific models, and models of emerging classes of systems such as service-oriented architectures and adaptive systems, as well as additional classes of systems and models that we cannot yet anticipate. As well as following the general trend toward greater use of models in development, though, research in model-based testing reflects greater understanding of the special role that models of software can play in test design and in combining conventional testing with analysis. A model is often the best way – perhaps the only way – to divide one property to be verified into two, one part that is best verified with static analysis and another part that is best verified with testing. Conformance testing of all kinds exploits models in this way, focusing analysis techniques where they are most necessary (e.g., nondeterministic scheduling decisions in concurrent software) and using testing to cost-effectively verify consistency between model and program. Models are also used to specify and describe system structure at levels of organization beyond those that are directly accommodated in conventional programming languages (e.g., components and subsystems). Analysis, and to a lesser extent testing, have been explicit concerns in development of architecture description languages. Still there remains a divide between models developed primarily for people to communicate and record design decisions (e.g., UML) and models developed primarily for verification (e.g., various FSM notations). Today we see a good deal of research re-purposing design models for test design, which involves adding or disambiguating the semantics of notations intended for human communication. A challenge for future design notations is to provide a better foundation for analysis and testing without sacrificing the characteristics that make them useful for communicating and recording design decisions. An important issue in modeling, and by extension in model-based testing, is how to use
multiple model “views” to together form a comprehensive model of a program. More work is needed on test design that uses more than one modeling view, or on the potential interplay between test specifications derived from different model views of the same program. As with many other areas of software testing and analysis, more empirical research is also needed to characterize the cost and effectiveness of model-based testing approaches. Perhaps even more than in other areas of testing research, this is not only a matter of carrying out experiments and case studies, but is at least as much a matter of understanding how to pose questions that can be effectively answered by experiments and whose answers generalize in useful ways.
Further Reading Myers’ classic text [Mye79] describes a number of techniques for testing decision structures. Richardson, O’Malley, and Tittle [ROT89] and Stocks and Carrington [SC96] among others attempt to generate test cases based on the structure of (formal) specifications. Beizer’s Black Box Testing [Bei95] is a popular presentation of techniques for testing based on control and data flow structure of (informal) specifications. Test design based on finite state machines has long been important in the domain of communication protocol development and conformance testing; Fujiwara, von Bochmann, Amalou, and Ghedamsi [FvBK+91] is a good introduction. Gargantini and Heitmeyer [GH99] describe a related approach applicable to software systems in which the finite state machine is not explicit but can be derived from a requirements specification. Generating test suites from context-free grammars is described by Celentano et al. [CCD+80] and apparently goes back at least to Hanford’s test generator for an IBM PL/I compiler [Han70]. The probabilistic approach to grammar-based testing is described by Sirer and Bershad [SB99], who use annotated grammars to systematically generate tests for Java virtual machine implementations. Heimdahl et al. [HDW04] provide a cautionary note regarding how naive model-based testing can go wrong, while a case study by Pretschner et al. [PPW+05] suggests that model based testing is particularly effective in revealing errors in informal specifications.
Related Topics Readers interested in testing based on finite state machines may proceed to Chapter 15, in which finite state models are applied to testing object-oriented programs.
Exercises Derive sets of test cases for functionality Maintenance from the FSM specification in Figure 14.2. 1. Derive a test suite that satisfies the Transition Coverage criterion.
2. Derive a test suite that satisfies the Single State Path Coverage criterion.
14.1
3. Indicate at least one element of the program that must be covered by a test suite satisfying the Single Transition Path Coverage, but need not be covered by a test suite that satisfies the Single State Path Coverage criterion. Derive a test case that covers that element. 4. Describe at least one element that must be covered by a test suite that satisfies both the Single Transition Path Coverage and Boundary Interior Loop Coverage criteria, but need not be covered by a test suite that satisfies the Transition Coverage and Single State Path Coverage criteria. Derive testsuite casederived that covers that element. Discuss how the atest for functionality Maintenance applying Transition
14.2
14.3
Coverage to the FSM specification of Figure 14.2 (Exercise 14.1) must be modified under the following assumptions. 1. How must it be modified if the implicit transitions ar error conditions? 2. How must it be modified if the implicit transitions are self-transitions? Finite state machine specifications are often augumented with variables that may be tested and changed by state transitions. The same system can often be described by a machine with more or fewer states, depending on how much information is represented by the states themselves and how much is represented by extra variables. For example, in Figure 5.9 (page 69), the state of the buffer (empty or not) is represented directly by the states, but we could also represent that information with a variable empty and merge states Empty buffer and Within line of the finite state machine into a single Gathering state to obtain a more compact finite state machine, as in this diagram:
For the following questions, consider only scalar variables with a limited set of possible values, like the Boolean variable empty in the example. 1. How can we systematically transform a test case for one version of the specification into a test suite for the other? Under what conditions is this transformation possible? Consider transformation in both directions, merging states by adding variables and splitting states to omit variables.
2. If a test suite satisfies the transition coverage criterion for the version with more states, will a corresponding test suite (converting each test case as you described in part (a)) necessarily satisfy the transition coverage criterion for the version with a suite that satisfies the transition coverage criterion for the version with fewer states? 3. Conversely, if a test suite satisfies the transition coverage criterion for the version of the specification with fewer states, will a corresponding test suite (converted as you described in part (a)) necessarily satisfy the transition coverage criterion for the version with more states? 4. How might you combine transition coverage with decision structure testing methods to select test suites independently from the information coded explicitly in the states or implicitly in the state variable?
Chapter 15: Testing Object-Oriented Software Systematic testing of object-oriented software is fundamentally similar to systematic testing approaches for procedural software: We begin with functional tests based on specification of intended behavior, add selected structural test cases based on the software structure, and work from unit testing and small-scale integration testing toward larger integration and then system testing. Nonetheless, the differences between procedural software and objectoriented software are sufficient to make specialized techniques appropriate.
Required Background Chapters 11, 12, 13, and 14 This chapter builds on basic functional, structural, and model-based testing techniques, including data flow testing techniques. Some basic techniques, described more thoroughly in earlier chapters, are recapped very briefly here to provide flexibility in reading order. Chapter 5 Many of the techniques described here employ finite state machines for modeling object state.
15.1 Overview Object-oriented software differs sufficiently from procedural software to justify reconsidering and adapting approaches to software test and analysis. For example, methods in objectoriented software are typically shorter than procedures in other software, so faults in complex intraprocedural logic and control flow occur less often and merit less attention in testing. On the other hand, short methods together with encapsulation of object state suggest greater attention to interactions among method calls, while polymorphism, dynamic binding, generics, and increased use of exception handling introduce new classes of fault that require attention. Some traditional test and analysis techniques are easily adapted to object-oriented software. For example, code inspection can be applied to object-oriented software much as it is to procedural software, albeit with different checklists. In this chapter we will be concerned mostly with techniques that require more substantial revision (like conventional structural testing techniques) and with the introduction of new techniques for coping with problems associated with object-oriented software.
15.2 Issues in Testing Object-Oriented Software The characteristics of object-oriented software that impact test design are summarized in the sidebar on page 273 and discussed in more detail below. The behavior of object-oriented programs is inherently stateful: The behavior of statedependent behavior a method depends not only on the parameters passed explicitly to the method, but also on the state of the object. For example, method CheckConfiguration() of class Model, shown in Figure 15.1, returns True or False depending on whether all components are bound to compatible slots in the current object state.
public class Model extends Orders.CompositeItem { 2 public String modelID; // Database key for slots 3 private int baseWeight; // Weight excluding optional compon 4 private int heightCm, widthCm, depthCm; // Dimensions if bo 5 private Slot[] slots; // Component slots 6 7 private boolean legalConfig = false; // memoized result of 8 private static final String NoModel = “NO MODEL SELECTED”; 12 … 13 /** Constructor, which should be followed by selectModel */ 14 public Model(Orders.Order order) { 15 super( order); 16 modelID = NoModel; 17 } 99 … 100 /** Is the current binding of components to slots a legal 101 configuration? Memoize the result for repeated calls / 102 public boolean isLegalConfiguration() { 103 if (! legalConfig) { 104 checkConfiguration(); 105 } 106 return legalConfig; 107 } 108 109 /** Are all required slots filled with compatible component 110 * It is impossible to assign an incompatible component, 111 * so just to check that every required slot is filled. */ 112 private void checkConfiguration() { 113 legalConfig = true; 114 for (int i=0; i
118 119 120 241 242
} } } … }
Figure 15.1: Part of a Java implementation of class Model. In object-oriented programs, public and private parts of a class (fields and methods) are distinguished. Private state and methods are inaccessible to external entities, which can only change or inspect private state by invoking public methods.[1] For example, the instance variable modelID of class Model in Figure 15.1 is accessible by external entities, but slots and legalConfig are accessible only within methods of the same class. The constructor Model() and the method checkConfiguration() can be used by external entities to create new objects and to check the validity of the current configuration, while method openDB() can be invoked only by methods of this class. Encapsulated information creates new problems in designing oracles and test cases. Oracles must identify incorrect (hidden) state, and test cases must exercise objects in different (hidden) states. Object-oriented programs include classes that are defined by extending or specializing other classes through inheritance. For example, class Model in Figure 15.1 extends class CompositeItem, as indicated in the class declaration. A child class can inherit variables and methods from its ancestors, overwrite others, and add yet others. For example, the class diagram of Figure 15.3 shows that class Model inherits the instance variables sku, units and parts, and methods validItem(), getUnitPrice() and getExtendedPrice(). It overwrites methods getHeightCm(), getWidthCm(), getDepthCm() and getWeightGm(). It adds the instance variables baseWeight, modelID, heightCm, widthCm, DepthCm, slots and legalConfig, and the methods selectModel(), deselect-Model(), addComponent(), removeComponent() and isLegalConfiguration().
Figure 15.3: An excerpt from the class diagram of the Chipmunk Web presence that shows the hierarchy rooted in class LineItem. Summary: Relevant Characteristics of Object-Oriented Software State Dependent Behavior Testing techniques must consider the state in which methods are invoked. Testing techniques that are oblivious to state (e.g., traditional coverage of control structure) are not effective in revealing state-dependent faults. Encapsulation The effects of executing object-oriented code may include outputs, modification of object state, or both. Test oracles may require access to private (encapsulated) information to distinguish between correct and incorrect behavior. Inheritance Test design must consider the effects of new and overridden methods on the behavior of inherited methods, and distinguish between methods that require new test cases, ancestor methods that can be tested by reexecuting existing test cases, and methods that do not need to be retested. Polymorphism and Dynamic Binding A single method call may be dynamically bound to different methods depending on the state of the computation. Tests must exercise different bindings to reveal failures that depend on a particular binding or on interactions between bindings for different calls. Abstract Classes Abstract classes cannot be directly instantiated and tested, yet they may be important interface elements in libraries and components. It is necessary to test
them without full knowledge of how they may be instantiated. Exception Handling Exception handling is extensively used in modern object-oriented programming. The textual distance between the point where an exception is thrown and the point where it is handled, and the dynamic determination of the binding, makes it important to explicitly test exceptional as well as normal control flow. Concurrency Modern object-oriented languages and toolkits encourage and sometimes even require multiple threads of control (e.g., the Java user interface construction toolkits AWT and Swing). Concurrency introduces new kinds of possible failures, such as deadlock and race conditions, and makes the behavior of a system dependent on scheduler decisions that are not under the tester’s control.
Inheritance brings in optimization issues. Child classes may share several methods with their ancestors. Sometimes an inherited method must be retested in the child class, despite not having been directly changed, because of interaction with other parts of the class that have changed. Many times, though, one can establish conclusively that the behavior of an inherited method is really unchanged and need not be retested. In other cases, it may be necessary to rerun tests designed for the inherited method, but not necessary to design new tests. Most object-oriented languages allow variables to dynamically change their type, as long as they remain within a hierarchy rooted at the declared type of the variable. For example, variable subsidiary of method getYTDPurchased() in Figure 15.4 can be dynamically bound to different classes of the Account hierarchy, and thus the invocation of method subsidiary.getYTDPurchased() can be bound dynamically to different methods.
1 public abstract class Account { 151 … 152 /** 153 * The YTD Purchased amount for an account is the YTD 154 * total of YTD purchases of all customers using this accoun 155 * plus the YTD purchases of all subsidiaries of this accoun 156 * currency is currency of this account. 157 */ 158 public int getYTDPurchased() { 159 160 if (ytdPurchasedValid) { return ytdPurchased; } 161 162 int totalPurchased = 0; 163 for (Enumeration e = subsidiaries.elements() ; e.hasMoreE 164 { 165 Account subsidiary = (Account) e.nextElement(); 166 totalPurchased += subsidiary.getYTDPurchased();
167 168 169 170 171 172 173 174 175 176 332 333
} for (Enumeration e = customers.elements(); e.hasMoreEleme { Customer aCust = (Customer) e.nextElement(); totalPurchased += aCust.getYearlyPurchase(); } ytdPurchased = totalPurchased; ytdPurchasedValid = true; return totalPurchased; } … }
Figure 15.4: Part of a Java implementation of Class Account. The abstract class is specialized by the regional markets served by Chipmunk into USAccount, UKAccount, JPAccount, EUAccount and OtherAccount, which differ with regard to shipping methods, taxes, and currency. A corporate account may be associated with several individual customers, and large companies may have different subsidiaries with accounts in different markets. Method getYTDPurchased() sums the year-to-date purchases of all customers using the main account and the accounts of all subsidiaries. Dynamic binding to different methods may affect the whole computation. Testing a call by considering only one possible binding may not be enough. Test designers need testing techniques that select subsets of possible bindings that cover a sufficient range of situations to reveal faults in possible combinations of bindings. Some classes in an object-oriented program are intentionally left incomplete and cannot be directly instantiated. These abstract classes[2] must be extended through subclasses; only subclasses that fill in the missing details (e.g., method bodies) can be instantiated. For example, both classes LineItem of Figure 15.3 and Account of Figure 15.4 are abstract. If abstract classes are part of a larger system, such as the Chipmunk Web presence, and if they are not part of the public interface to that system, then they can be tested by testing all their child classes: classes Model, Component, CompositeItem, and SimpleItem for class LineItem and classes USAccount, UKAccount, JPAccount, EUAccount and OtherAccount for class Account. However, we may need to test an abstract class either prior to implementing all child classes, for example if not all child classes will be implemented by the same engineers in the same time frame, or without knowing all its implementations, for example if the class is included in a library whose reuse cannot be fully foreseen at development time. In these cases, test designers need techniques for selecting a representative set of instances for testing the abstract class. Exceptions were originally introduced in programming languages independently of objectoriented features, but they play a central role in modern object-oriented programming languages and in object-oriented design methods. Their prominent role in object-oriented
programs, and the complexity of propagation and handling of exceptions during program execution, call for careful attention and specialized techniques in testing. The absence of a main execution thread in object-oriented programs makes them well suited for concurrent and distributed implementations. Although many object-oriented programs are designed for and executed in sequential environments, the design of object-oriented applications for concurrent and distributed environments is becoming very frequent. Object-oriented design and programming greatly impact analysis and testing. However, test designers should not make the mistake of ignoring traditional technology and methodologies. A specific design approach mainly affects detailed design and code, but there are many aspects of software development and quality assurance that are largely independent of the use of a specific design approach. In particular, aspects related to planning, requirements analysis, architectural design, deployment and maintenance can be addressed independently of the design approach. Figure 15.5 indicates the scope of the impact of object-oriented design on analysis and testing.
Figure 15.5: The impact of object-oriented design and coding on analysis and testing. [1]Object-oriented
languages differ with respect to the categories of accessibility they provide. For example, nothing in Java corresponds exactly to the “friend” functions in C++ that are permitted to access the private state of other objects. But while details vary, encapsulation of state is fundamental to the object-oriented programming paradigm, and all major object-oriented languages have a construct comparable to Java’s private field declarations. [2]Here
we include the Java interface construct as a kind of abstract class.
15.3 An Orthogonal Approach to Test Testing all aspects of object-oriented programs simultaneously would be difficult and expensive; fortunately it is also unnecessary. It is more cost-effective to address different features individually, using appropriate techniques for each, and to explicitly address significant interactions (e.g., between inheritance and state-dependent behavior) rather than blindly exploring all different feature combinations. The proper blend of techniques depends on many factors: application under test, development approach, team organization, application criticality, development environment and the implementation languages, use of design and language features, and project timing and resource constraints. Nonetheless, we can outline a general approach that works in stages, from single classes to class and system interactions. A single “stage” is actually a set of interrelated test design and test execution activities. The approach is summarized in the sidebar on page 281 and described in more detail in this section in the order that tests related to a particular class would be executed, although test design and execution activities are actually interleaved and distributed through development. The smallest coherent unit for unit testing of object-oriented testing is the class. Test designers can address inheritance, state-dependent behavior and exceptions with intraclass testing. For example, when testing class Model of Figure 15.3, test designers may first use testing histories (see Section 15.10) to infer that method getExtendedPrice need not be retested, since it has already been tested in class LineItem. On the other hand, test designers must derive new test cases for the new methods and for those affected by the modifications introduced in class Model. After considering individual methods, test designers can proceed to design functional test cases from the statechart specification of class Model (see Section 15.5) and structural test cases from data flow information (see Section 15.7). To execute test cases, test designers may decide to use equivalent scenarios as oracles (see Section 15.8). Test designers will then create test cases for exceptions thrown or handled by the class under test (see Section 15.12). Class Model does not make polymorphic calls, so no additional test cases need be designed to check behavior with variable bindings to different classes. Integration (interclass) tests must be added to complete the testing for hierarchy, polymorphism, and exception-related problems. For example, when testing integration of class Model within the Chipmunk Web presence, test designers will identify class Slot as a predecessor in the integration order and will test it first, before testing its integration with class Model (see Sections 15.5 and 15.7). They will also derive test cases for completing the test of exceptions (see Section 15.12) and polymorphism (see Section 15.9). System and acceptance testing check overall system behavior against user and system requirements. Since these requirements are (at least in principle) independent of the design approach, system and acceptance testing can be addressed with traditional techniques. For example, to test the business logic subsystem of the Chipmunk Web presence, test designers may decide to derive test cases from functional specifications using category-
partition and catalog based methods (see Chapter 11). Steps in Object-Oriented Software Testing Object-oriented testing can be broken into three phases, progressing from individual classes toward consideration of integration and interactions. Intraclass Testing classes in isolation (unit testing) 1. If the class-under-test is abstract, derive a set of instantiations to cover significant cases. Instantiations may be taken from the application (if available) and/or created just for the purpose of testing. 2. Design test cases to check correct invocation of inherited and overridden methods, including constructors. If the class-under-test extends classes that have previously been tested, determine which inherited methods need to be retested and which test cases from ancestor classes can be reused. 3. Design a set of intraclass test cases based on a state machine model of specified class behavior. 4. Augment the state machine model with structural relations derived from class source code and generate additional test cases to cover structural features. 5. Design an initial set of test cases for exception handling, systematically exercising exceptions that should be thrown by methods in the class under test and exceptions that should be caught and handled by them. 6. Design an initial set of test cases for polymorphic calls (calls to superclass or interface methods that can be bound to different subclass methods depending on instance values). Interclass Testing class integration (integration testing) 1. Identify a hierarchy of clusters of classes to be tested incrementally. 2. Design a set of functional interclass test cases for the cluster-under-test. 3. Add test cases to cover data flow between method calls. 4. Integrate the intraclass exception-handling test sets with interclass exceptionhandling test cases for exceptions propagated across classes. 5. Integrate the polymorphism test sets with tests that check for interclass interactions of polymorphic calls and dynamic bindings. System and Acceptance Apply standard functional and acceptance testing techniques to larger components and the whole system.
15.4 Intraclass Testing Unit and integration testing aim to expose faults in individual program units and in their interactions, respectively. The meaning of “unit” is the smallest development work assignment for a single programmer that can reasonably be planned and tracked. In procedural programs, individual program units might be single functions or small sets of strongly related functions and procedures, often included in a single file of source code. In object-oriented programs, small sets of strongly related functions or procedures are naturally identified with classes, which are generally the smallest work units that can be systematically tested. Treating an individual method as a unit is usually not practical because methods in a single class interact by modifying object state and because the effect of an individual method is often visible only through its effect on other methods. For example, method check configuration of class computer, shown in Figure 15.1, can be executed only if the object is in a given state, and its result depends on the current configuration. The method may execute correctly in a given state (i.e., for a given configuration), but may not execute correctly in a different state (e.g., accepting malformed configurations or rejecting acceptable configurations). Moreover, method check configuration might produce an apparently correct output (return value) but leave the object in an incorrect state.
15.5 Testing with State Machine Models Since the state of an object is implicitly part of the input and output of methods, we need a way to systematically explore object states and transitions. This can be guided by a state machine model, which can be derived from module specifications. A state machine model can be extracted from an informal, natural language specification of intended behavior, even when the specification does not explicitly describe states and transitions. States can be inferred from descriptions of methods that act differently or return different results, depending on the state of the object; this includes any description of when it is allowable to call a method. Of course, one wants to derive only a reasonable number of abstract states as representatives of a much larger number of concrete states, and some judgment is required to choose the grouping. For example, if an object kept an integer count, we might choose “zero” and “nonzero” as representative states, rather than creating a different state for every possible value. The principle to observe is that we are producing a model of how one method affects another, so the states should be refined just enough to capture interactions. Extracting a state machine from an informal specification, and then creating test cases (sequences of method calls) to cover transitions in that model, are illustrated in the sidebar on page 283. Sometimes an explicit state machine model is already available as part of a specification or design. If so, it is likely to be in the form of a statechart (also known as a state diagram in the UML family of notations). Statecharts include standard state transition diagrams, but also provide hierarchical structuring constructs. The structuring facilities of statecharts can be used to organize and hide complexity, but this complexity must be exposed to be tested. From Informal Specs to Transition Coverage An Informal Specification of Class Slot Slot represents a configuration choice in all instances of a particular model of computer. It may or may not be implemented as a physical slot on a bus. A given model may have zero or more slots, each of which is marked as required or optional. If a slot is marked as “required,” it must be bound to a suitable component in all legal configurations. Class Slot offers the following services: Incorporate: Make a slot part of a model, and mark it as either required or optional. All instances of a model incorporate the same slots. Example: We can incorporate a required primary battery slot and an optional secondary battery slot on the Chipmunk C20 laptop that includes two battery slots. The C20 laptop may then be sold with one battery or two batteries, but it is not sold without at least the primary battery. Bind: Associate a compatible component with a slot. Example: We can bind slot
primary battery to a Blt4, Blt6, or Blt8 lithium battery or to a Bcdm4 nickel cadmium battery. We cannot bind a disk drive to the battery slot. Unbind: The unbind operation breaks the binding of a component to a slot, reversing the effect of a previous bind operation. IsBound: Returns true if a component is currently bound to a slot, or false if the slot is currently empty. The Corresponding Finite State Machine A simple analysis of the informal specification of class Slot allows one to identify states and transitions. Often an analysis of natural language specifications will reveal ambiguities that must be resolved one way or the other in the model; these may suggest additional test cases to check the interpretation, or lead to refinement of the specification, or both. For class slot, we infer that the bind operation makes sense only after the slot has been incorporated in a model, and that it is initially empty.
The Generated Test Case Specifications A single test case will be given as a sequence of method calls. For class Slot, the following test cases suffice to execute each transition in the state machine model: TC-1 incorporate, isBound, bind, isBound TC-2 incorporate, unBind, bind, unBind, isBound
The most common structuring mechanism in statecharts is grouping of states in superstates (also called OR-states). A transition from a superstate is equivalent to a transition from every state contained within it. A transition to a superstate is equivalent to a transition to the initial state within the superstate. We can obtain an ordinary state machine by “flattening” the statechart hierarchy, replacing transitions to and from superstates to transitions among elementary states. Figure 15.6 shows a statechart specification for class Model of the business logic of the Chipmunk Web presence. Class Model provides methods for selecting a computer model and a set of components to fill logical and physical slots. The state modelSelected is decomposed into its two component states, with entries to modelSelected directed to the default initial state workingConfiguration.
Figure 15.6: Statechart specification of class Model. Table 15.1 shows a set of test cases that cover all transitions of the finite state machine of Figure 15.7, a flattened version of the statechart of Figure 15.6. Notice that transition selectModel of the statechart corresponds to a single transition in the FSM, since entry to the superstate is directed to the default initial state, while transition deselectModel of the statechart corresponds to two transitions in the FSM, one for each of the two children states, since the superstate can be exited while in either component state.
Figure 15.7: Finite state machine corresponding to the statechart of Figure 15.6. Table 15.1: A set of test cases that satisfies the transition coverage criterion for the statechart of Figure 15.6. Open table as spreadsheet
Test Case TCA selectModel(M1) addComponent(S1,C1) addComponent(S2,C2) isLegalConfiguration()
Test Case TCB selectModel(M1) deselectModel() selectModel(M2) addComponent(S1,C1) addComponent(S2,C2) removeComponent(S1) isLegalConfiguration()
Test Case TCD selectModel(M1) addComponent(S1,C1)
Test Case TCE selectModel(M1)
Test Case TCC selectModel(M1) addComponent(S1,C1) removeComponent(S1) addComponent(S1,C2) isLegalConfiguration()
addComponent(S2,C2) addComponent(S3,C3) deselectModel() selectModel(M1) addComponent(S1,C1) isLegalConfiguration()
addComponent(S1,C1) addComponent(S2,C2) addComponent(S3,C3) removeComponent(S2) addComponent(S2,C4) isLegalConfiguration()
In covering the state machine model, we have chosen sets of transition sequences that together exercise each individual transition at least once. This is the transition adequacy criterion introduced in Chapter 14. The stronger history-sensitive criteria described in that chapter are also applicable in principle, but are seldom used because of their cost. Even transition coverage may be impractical for complex statecharts. The number of states and transitions can explode in “flattening” a statechart that represents multiple threads of control. Unlike flattening of ordinary superstates, which leaves the number of elementary states unchanged while replicating some transitions, flattening of concurrent state machines (so-called AND-states) produces new states that are combinations of elementary states. Figure 15.8 shows the statechart specification of class Order of the business logic of the Chipmunk Web presence. Figure 15.9 shows the corresponding “flattened” state machine. Flattening the AND-state results in a number of states equal to the Cartesian product of the elementary states (3 × 3 = 9 states) and a corresponding number of transitions. For instance, transition add item that exits state not scheduled of the statechart corresponds to three transitions exiting the states not schedXcanc no fee, not schedXcanc fee, and not schedXnot canc, respectively. Covering all transitions at least once may result in a number of test cases that exceeds the budget for testing the class. In this case, we may forgo flattening and use simpler criteria that take advantage of the hierarchical structure of the statechart.
Figure 15.8: Statechart specification of class Order. This is a conceptual model in which both methods of class Order and method calls by class Order are represented as transitions with names that differ from method names in the implementation (e.g., 5DaysBeforeShipping is not a legal method or field name).
Figure 15.9: Finite state machine corresponding to the statechart of Figure 15.8. Table 15.2 shows a test suite that satisfies the simple transition coverage adequacy criterion, which requires the execution of all transitions that appear in the statechart. The criterion requires that each statechart transition is exercised at least once, but does not
guarantee that transitions are exercised in all possible states. For example, transition add item, which leaves the initial state, is exercised from at least one substate, but not from all possible substates as required by the transition coverage adequacy criterion. Table 15.2: A test suite that satisfies the simple transition coverage adequacy criterion for the statechart of Figure 15.8. Transitions are indicated without parameters for simplicity. Open table as spreadsheet
Test Case TCA add_item() add_item() package() get_shipping_cost() get_discount() purchase() place_order() 24_hours() 5_days() schedule() ship() deliver()
Test Case TCD add_item() add_item() package() get_shipping_cost() get_discount() purchase() place_order() remove_item() add_item() package() get_shipping_cost() get_discount() purchase() place_order() 24_hours()
Test Case TCB add_item() add_item() remove_item() add_item() package() get_shipping_cost() get_discount() purchase() place_order() 24_hours() 5_days() schedule() ship() deliver()
Test Case TCE add_item() add_item() package() get_shipping_cost() get_discount() purchase() place_order() schedule() suspend() 5_days() schedule() ship()
Test Case TCC add_item() add_item() package() get_shipping_cost() get_discount() purchase() place_order() add_item() package() get_shipping_cost() get_discount() purchase() place_order() 24_hours() 5_days() schedule() ship() deliver()
Test Case TCF add_item() add_item() package() get_shipping_cost() get_discount() purchase() place_order() schedule() cancel()
5_days() schedule() ship() deliver()
deliver()
Test Case TCG add_item() add_item() package() get_shipping_cost() get_discount() purchase() place_order() schedule() ship() address_unknown()
Test Case TCH add_item() add_item() package() get_shipping_cost() get_discount() purchase() place_order() schedule() 5_days() address_unknown()
Test Case TCI add_item() add_item() package() get_shipping_cost() get_discount() purchase() place_order() schedule() 24_hours() cancel()
15.6 Interclass Testing Interclass testing is the first level of integration testing for object-oriented software. While intraclass testing focuses on single classes, interclass testing checks interactions among objects of different classes. As in integration testing of imperative programs, test designers proceed incrementally, starting from small clusters of classes. Since the point of interclass testing is to verify interactions, it is useful to model potential interactions through a use/include relation. Classes A and B are related by the use/include relation if objects of class A make method calls on objects of class B, or if objects of class A contain references to objects of class B. Inheritance is ignored (we do not consider a subclass to use or include its ancestors), and abstract classes, which cannot directly participate in interactions, are omitted. Derivation of the use/include relation from a conventional UML class diagram is illustrated in Figures 15.10 and 15.11.
Figure 15.10: Part of a class diagram of the Chipmunk Web presence. Classes Account, LineItem, and CSVdb are abstract.
Figure 15.11: Use/include relation for the class diagram in Figure 15.10. Abstract classes are not included. Two classes are related if one uses or includes the other. Classes that are higher in the diagram include or use classes that are lower in the diagram. Interclass testing strategies usually proceed bottom-up, starting from classes that depend on no others. The implementation-level use/include relation among classes typically parallels the more abstract, logical depends relation among modules (see sidebar on page 292), so a bottom-up strategy works well with cluster-based testing. For example, we can start integrating class SlotDB with class Slot, and class Component with class ComponentDB, and then proceed incrementally integrating classes ModelDB and Model, up to class Order. Dependence The hierarchy of clusters for interclass testing is based on a conceptual relation of dependence, and not directly on concrete relations among implementation classes (or implementation-level design documentation). Module A depends on module B if the functionality of B must be present for the functionality of A to be provided. If A and B are implemented as classes or clusters of closely related classes, it is likely that the logical depends relation will be reflected in concrete relations among the classes. Typically, the class or classes in A will either call methods in the class or classes in B, or classes in A will have references to classes in B forming a contains relation among their respective objects. Concrete relations among classes do not always indicate dependence. It is common for contained objects to have part-of relations with their ancestors in the containment hierarchy, but the dependence is normally from container to contained object and not vice versa. It is also common to find calls from framework libraries to methods that use those libraries. For example, the SAX API for parsing XML is an event-driven parsing
framework, which means the parsing library makes calls (through interfaces) on methods provided by the application. This style of event handling is most familiar to Java programmers through the standard Java graphical user interface libraries. It is clear that the application depends on the library and not vice versa. The depends relation is as crucial to other software development processes as it is to testing. It is essential to building a system as a set of incremental releases, and to scheduling and managing the construction of each release. The depends relation may be documented in UML package diagrams, and even if not documented explicitly it is surely manifest in the development build order. Test designers may (and probably should) be involved in defining the build order, but should not find themselves in the position of discovering or re-creating it after the fact.
Well-designed systems normally have nearly acyclic dependence relations, with dependence loops limited to closely related clusters. When there are larger loops in the relation, or when a use/include relation among classes runs contrary to the depends relation (e.g., an “up-call” to an ancestor in the depends relation), the loop can be broken by substituting a stub for the ancestor class. Thus, we always work with an acyclic graph of clusters. In principle, while climbing the dependence relation, a thorough interclass testing should consider all combinations of possible interactions. If, for example, a test case for class Order includes a call to a method of class Model, and the called method calls a method of class Slot, each call should be exercised for all relevant states of the different classes, as identified during intraclass testing. However, this suffers from the same kind of combinatorial explosion that makes flattening concurrent state diagrams impractical. We need to select a subset of interactions among the possible combinations of method calls and class states. An arbitrary or random selection of interactions may be an acceptable solution, but in addition one should explicitly test any significant interaction scenarios that have been previously identified in design and analysis. Interaction scenarios may have been recorded in the form of UML interaction diagrams, expressed as sequence or collaboration diagrams. These diagrams describe interactions among objects and can be considered essentially as test scenarios created during the course of design. In addition to testing the scenarios spelled out in sequence or collaboration diagrams, the test designer can vary those scenarios to consider illegal or unexpected interaction sequences. For example, replacing a single interaction in a sequence diagram with another interaction that should not be permitted at that point yields a test case that checks error handling. Figure 15.12 shows a possible pattern of interactions among objects, when a customer assembling an order O first selects the computer model C20, then adds a hard disk HD60 that is not compatible with the slots of the selected model, and then adds “legal” hard disk HD20. The sequence diagram indicates the sequence of interactions among objects and
suggests possible testing scenarios. For example, it suggests adding a component after having selected a model. In other words, it indicates interesting states of objects of type ModelDB and Slots when testing class Model.
Figure 15.12: A (partial) sequence diagram that specifies the interactions among objects of type Order, Model, ModelDB, Component, ComponentDB, Slots, and SlotDB, to select a computer, add an illegal component, and then add a legal one. Unlike statecharts, which should describe all possible sequences of transitions that an object can undergo, interaction diagrams illustrate selected interactions that the designers considered significant because they were typical, or perhaps because they were difficult to understand. Deriving test cases from interaction diagrams is useful as a way of choosing some significant cases among the enormous variety of possible interaction sequences, but it is insufficient as a way of ensuring thorough testing. Integration tests should at the very least repeat coverage of individual object states and transitions in the context of other parts of the cluster under test.
15.7 Structural Testing of Classes In testing procedural code, we take specifications as the primary source of information for test design (functional testing), and then we analyze implementation structure and add test cases as needed to cover additional variation (structural testing). The same approach applies to object-oriented programs and for the same reasons. The techniques described in previous sections are all based on specification of intended behavior. They should be augmented (but never replaced) by structural techniques. If we compare the implementation of class Model shown in Figures 15.1 and 15.2 with its specification in Figures 15.3 and 15.6, we notice that the code uses an instance variable legalConfig and an internal (private) method checkConfiguration to optimize the implementation of method isLegalConfiguration. The functional test cases shown in Table 15.1 do not include method checkConfiguration, though some of them will call it indirectly through isLegalConfiguration. An alert test designer will note that every modification of the object state that could possibly invalidate a configuration should reset the hidden legalConfig variable to False, and will derive structural test cases to cover behaviors not sufficiently exercised by functional test cases. public 61 … 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85
1
class Model extends Orders.CompositeItem {
/** Bind a component to a slot. * @param slotIndex Which slot (integer index)? * @param sku Key to component database. * Choices should be constrained by web interface, so we do * need to be graceful in handling bogus parameters. */ public void addComponent(int slotIndex, String sku) { Slot slot =slots[slotIndex]; if (componentDB.contains(sku)) { Component comp = new Component(order, sku); if (comp.isCompatible(slot.slotID)) { slot.bind(comp); // Note this cannot have made the // configuration illegal. } else { slot.unbind(); legalConfig = false; } } else { slot.unbind(); legalConfig = false; } }
86 87 88 89 90 91 92 93 215 216
/** Unbind a component from a slot. */ public void removeComponent(int slotIndex) { // assert slotIndex in 0..slots.length if (slots[slotIndex].isBound()) { slots[slotIndex].unbind(); } legalConfig = false; 94 } … }
Figure 15.2: More of the Java implementation of class Model. Because of the way method isLegalConfig is implemented (see Figure 15.1), all methods that modify slots must reset the private variable legalConfig. The chief difference between functional testing techniques for object-oriented software and their counterparts for procedural software (Chapters 10, 11, and 14) is the central role of object state and of sequences of method invocations to modify and observe object state. Similarly, structural test design must be extended beyond consideration of control and data flow in a single method to take into account how sequences of method invocations interact. For example, tests of isLegalConfiguration would not be sufficient without considering the prior state of private variable legalConfig. Since the state of an object is comprised of the values of its instance variables, the number of possible object states can be enormous. We might choose to consider only the instance variables that do not appear in the specification, and add only those to the state machine representation of the object state. In the class Model example, we will have to add only the state of the Boolean variable legalConfig, which can at most double the number of states (and at worst quadruple the number of transitions). While we can model the concrete values of a single Boolean variable like legalConfig, this approach would not work if we had a dozen such variables, or even a single integer variable introduced in the implementation. To reduce the enormous number of states obtained by considering the combinations of all values of the instance variables, we could select a few representative values. Another way to reduce the number of test cases based on interaction through instance variable values while remaining sensitive enough to catch many common oversights is to model not the values of the variables, but the points at which the variables receive those values. This is the same intuition behind data flow testing described in Chapter 13, although it requires some extension to cover sequences in which one method defines (sets) a variable and another uses that variable. Definition-use pairs for the instance variables of an object are computed on an intraclass control flow graph that joins all the methods of a single class, and thus allows pairing of definitions and uses that occur in different methods. Figure 15.13 shows a partial intraclass control flow graph of class Model. Each method is modeled with a standard control flow graph (CFG), just as if it were an independent
procedure, except that these are joined to allow paths that invoke different methods in sequence. To allow sequences of method calls, the class itself is modeled with a node class Model connected to the CFG of each method. Method Model includes two extra statements that correspond to the declarations of variables legalConfig and modelDB that are initialized when the constructor is invoked.[3]
Figure 15.13: A partial intraclass control flow graph for the implementation of class Model in Figures 15.1 and 15.2. Sometimes definitions and uses are made through invocation of methods of other classes. For example, method addComponent calls method contains of class componentDB. Moreover, some variables are structured; for example, the state variable slot is a complex object. For the moment, we simply “unfold” the calls to external methods, and we treat arrays and objects as if they were simple variables. A test case to exercise a definition-use pair (henceforth DU pair) is a sequence of method invocations that starts with a constructor, and includes the definition followed by the use without any intervening definition (a definition-clear path). A suite of test cases can be designed to satisfy a data flow coverage criterion by covering all such pairs. In that case we say the test suite satisfies the all DU pairs adequacy criterion. Consider again the private variable legalConfig in class Model, Figures 15.1 and 15.2. There are two uses of legalConfig, both in method isLegalConfiguration, one in the if and one in the return statement; and there are several definitions in methods addComponent, removeComponent, checkConfiguration and in the constructor, which initializes legalConfig to False. The all DU pairs adequacy criterion requires a test case to exercise each definition followed by each use of legalConfig with no intervening definitions. Specifications do not refer to the variable legalConfig and thus do not directly consider method interactions through legalConfig or contribute to defining test cases to exercise such
interactions. This is the case, for example, in the invocation of method checkConfiguration in isLegalConfiguration: The specification suggests that a single invocation of method isLegalConfiguration can be sufficient to test the interactions involving this method, while calls to method checkConfiguration in isLegalConfiguration indicate possible failures that may be exposed only after two calls of method isLegalConfiguration. In fact, a first invocation of isLegalConfiguration with value True for legalConfig implies a call to checkConfiguration and consequent new definitions of legalConfig. Only a second call to isLegalConfiguration would exercise the use of the new value in the if statement, thus revealing failures that may derive from bad updates of legalConfig in checkConfiguration. The all DU pairs adequacy criterion ensures that every assignment to a variable is tested at each of the uses of that variable, but like other structural coverage criteria it is not particularly good at detecting missing code. For example, if the programmer omitted an assignment to legalConfig, there would be no DU pair connecting the missing assignment to the use. However, assignments to legalConfig are correlated with updates to slots, and all DU pairs coverage with respect to slots is likely to reveal a missing assignment to the Boolean variable. Correlation among assignments to related fields is a common characteristic of the structure of object-oriented software. Method calls and complex state variables complicate data flow analysis of object-oriented software, as procedure calls and structured variables do in procedural code. As discussed in Chapters 6 and 13, there is no universal recipe to deal with interclass calls. Test designers must find a suitable balance between costs and benefits. A possible approach to deal with interclass calls consists in proceeding incrementally following the dependence relation, as we did for functional interclass testing. The dependence relation that can be derived from code may differ from the dependence relation derived from specifications. However, we can still safely assume that well-designed systems present at most a small number of easily breakable cycles. The dependencies of the implementation and specification of class Model are the same and are shown in Figure 15.11. Leaf classes of the dependence hierarchy can be analyzed in isolation by identifying definitions and uses of instance variables, as just shown. The data flow information collected on leaf classes can be summarized by marking methods that access but do not modify the state as Inspectors; methods that modify, but do not otherwise access the state, as Modifiers; and methods that both access and modify the state as Inspector/Modifiers. When identifying inspectors, modifiers and inspector/modifiers, we consider the whole object state. Thus, we mark a method as inspector/modifier even if it uses just one instance variable and modifies a different one. This simplification is crucial to scalability, since distinguishing uses and definitions of each individual variable would quickly lead to an unmanageable amount of information while climbing the dependence hierarchy. If methods contain more than one execution path, we could summarize the whole method as an inspector, modifier, or inspector/modifier, or we could select a subset of paths to be
considered independently. A single method might include Inspector, Modifier, and Inspector/Modifier paths. Once the data flow information of leaf classes has been summarized, we can proceed with classes that only use or contain leaf classes. Invocations of modifier methods and inspector/modifiers of leaf classes are considered as definitions. Invocations of inspectors and inspector/modifiers are treated as uses. When approximating inspector/modifiers as uses, we assume that the method uses the values of the instance variables for computing the new state. This is a common way of designing methods, but some methods may fall outside this pattern. Again, we trade precision for scalability and reduced cost. We can then proceed incrementally analyzing classes that depend only on classes already analyzed, until we reach the top of the hierarchy. In this way, each class is always considered in isolation, and the summary of information at each step prevents exponential growth of information, thus allowing large classes to be analyzed, albeit at a cost in precision. Figure 15.14 shows the summary information for classes Slot, ModelDB, and Model. The summary information for classes Slot and ModelDB can be used for computing structural coverage of class Model without unfolding the method calls. The summary information for class Model can be used to compute structural coverage for class Order without knowing the structure of the classes used by class Order. Method checkConfiguration is not included in the summary information because it is private. The three paths in checkConfiguration are included in the summary information of the calling method isLegalConfiguration. Class Slot Slot() bind()
modifier modifier
unbind()
modifier
isBound()
inspector
Class ModelDB ModelDB()
modifier
getModel()
inspector
findModel()
inspector
Class Model
Model()
modifier
selectModel()
modifier
deselectModel()
modifier
addComponent() [1,2,8,9,10]
inspector/modifier
addComponent() [1,2,3,4,5,6,10]
inspector/modifier
addComponent() [1,2,3,4,7,10]
inspector/modifier
removeComponent() [1,2,3,4,5]
inspector/modifier
removeComponent() [1,2,4,5]
inspector/modifier
isLegalConfiguration() [1,2,3,[1,2,3,4,9],4]
inspector/modifier
isLegalConfiguration() [1,2,3,[1,2,3,4,5,6,7,4,9],4]
inspector/modifier
isLegalConfiguration() [1,2,3,[1,2,3,4,5,6,7,8,4,9],4]
inspector/modifier
isLegalConfiguration() [1,2,4]
modifier
Figure 15.14: Summary information for structural interclass testing for classes Slot, ModelDB, and Model. Lists of CFG nodes in square brackets indicate different paths, when methods include more than one part. While summary information is usually derived from child classes, sometimes it is useful to provide the same information without actually performing the analysis, as we have done when analyzing class Model. This is useful when we cannot perform data flow analysis on the child classes, as when child classes are delivered as a closed component without source code, or are not available yet because the development is still in progress. [3]We
have simplified Figure 15.13 by omitting methods getHeightCm, getWidthCm, getDepthCm, and getWeightGm, since they depend only on the constructor and do not affect other methods. Exception handlers are excluded since they will be treated separately, as described in Section 15.12.
15.8 Oracles for Classes Unit (intraclass) and integration (interclass) testing require suitable scaffolding to exercise the classes under test (drivers and stubs) and to inspect the test results (oracles). Constructing stubs and drivers for object-oriented software is essentially similar to the same task for procedural programs, and as in procedural programs, stubs can be avoided to the extent that the order of test execution is aligned with the build order of the software system. Oracles, however, can be more difficult to construct, owing to encapsulation of object state. The effect of executing a method or a whole sequence of methods in a test case is not only the outputs produced, but also the state of the objects after execution. For example, if method deselectModel of class Model does not clear the array slots, it is erroneous, even if it produces the expected visible outputs. Thus, oracles need to check the validity of both output and state. Unfortunately for the oracle builder, though, the state of objects may not be directly accessible. For example, variable slots is private and thus cannot be directly accessed by an oracle outside the class under test. One approach to building oracles is to break the encapsulation, for example, by modifying the source code to allow inspection of private variables. If we violate encapsulation by modifying code just for the purpose of testing, rather than leaving the modifications in the actual delivered code, then we risk differences in behavior between what is tested and what is used. We may mask faults, or we may inadvertently insert faults not present in the original code, particularly if we make modifications by hand. Even a small difference in performance can be important in a real-time system or in a multi-threaded system sensitive to scheduler decisions. Modifications that remain in the code, or (better) design rules that require programmers to provide observability interfaces, avoid discrepancies between the production code and the tested code. This is a particularly attractive option if the interface for observing object state can be separated from the main class, as one can for example do with a C++ friend class.[4] An observability interface can be a collection of observer methods, or a single method to produce a representation of the full object state. Often an interface that produces a readable, canonical representation of an object value will be useful in debugging as well as in testing. A second alternative is not to reveal the internal state of an object per se, but to provide a way of determining whether two objects are equivalent. Here “equivalent” does not mean that the internal states of two objects are identical, but that they represent the same abstract value. For example, we might consider the Java Vector class as representing a sequence. If so, then not only might two vectors with different capacities be considered equivalent, but we might even consider a vector object and a linked list object to be equivalent if they contain the same elements in the same order. An (abstract) check for equivalence can be used in a test oracle if test cases exercise two sequences of method calls that should (or should not) produce the same object state. Comparing objects using this equivalent scenarios approach is particularly suitable when the
classes being tested are an instance of a fairly simple abstract data type, such as a dictionary structure (which includes hash tables, search trees, etc.), or a sequence or collection. Table 15.3 shows two sequences of method invocations, one equivalent and one nonequivalent to test case TCE for class Model. The equivalent sequence is obtained by removing “redundant” method invocations – invocations that brings the system to a previous state. In the example, method deselectModel cancels the effect of previous invocations of method selectModel and addComponent. The nonequivalent sequence is obtained by selecting a legal subset of method invocations that bring the object to a different state. Table 15.3: Equivalent and nonequivalent scenarios (invocation sequences) for test case TCE from Table 15.1 for class Model. Test Case TCE selectModel(M1) addComponent(S1,C1) addComponent(S2,C2) isLegalCon.guration() deselectModel() selectModel(M2) addComponent(S1,C1) isLegalCon.guration()
Scenario TCE1 selectModel(M2) addComponent(S1,C1) isLegalCon.guration() EQUIVALENT
Scenario TCE2 selectModel(M2) addComponent(S1,C1) addComponent(S2,C2) isLegalConfiguration() NONEQUIVALENT
Producing equivalent sequences is often quite simple. While finding nonequivalent sequences is even easier, choosing a few good ones is difficult. One approach is to hypothesize a fault in the method that “generated” the test case, and create a sequence that could be equivalent if the method contained that fault. For example, test case TCE was designed to test method deselectModel. The nonequivalent sequence of Table 15.3 leads to a state that could be produced if method deselectModel did not clear all slots, leaving component C2 bound to slot S2 in the final configuration. One sequence of method invocations is equivalent to another if the two sequences lead to the same object state. This does not necessarily mean that their concrete representation is bit-for-bit equal. For example, method addComponent binds a component to a slot by creating a new Slot object (Figure 15.2). Starting from two identical Model objects, and calling addComponent on both with exactly the same parameters, would result in two objects that represent the same information but that nonetheless would contain references to distinct Slot objects. The default equals method inherited from class Object, which makes a bit-for-bit comparison, would not consider them equivalent. A good practice is to add a suitable observer method to a class (e.g., by overriding the default equals method in Java). [4]A
“friend” class in C++ is permitted direct access to private variables in another class. There is no direct equivalent in Java or SmallTalk, although in Java one could obtain a somewhat similar effect by using package visibility for variables and placing oracles in the
same package.
15.9 Polymorphism and Dynamic Binding Limited use of polymorphism and dynamic binding is easily addressed by unfolding polymorphic calls, considering each method that can be dynamically bound to each polymorphic call. Complete unfolding is impractical when many references may each be bound to instances of several subclasses. Consider, for example, the code fragment in Figure 15.15. Object Account may by an instance of any of the classes USAccount, UKAccount, EUAccount, JPAccount, or OtherAccount. Method validateCredit can be dynamically bound to methods validateCredit of any of the classes EduCredit, BizCredit, or IndividualCredit, each implementing different credit policies. Parameter creditCard may be dynamically bound to VISACard, AmExpCard, or ChipmunkCard, each with different characteristics. Even in this simple example, replacing the calls with all possible instances results in 45 different cases (5 possible types of account × 3 possible types of credit × 3 possible credit cards).
1 abstract class Credit { 15 … 16 abstract boolean validateCredit( Account a, int amt, CreditC 60 … 61 } Figure 15.15: A method call in which the method itself and two of its parameters can be dynamically bound to different classes. The explosion in possible combinations is essentially the same combinatorial explosion encountered if we try to cover all combinations of attributes in functional testing, and the same solutions are applicable. The combinatorial testing approach presented in Chapter 11 can be used to choose a set of combinations that covers each pair of possible bindings (e.g., Business account in Japan, Education customer using Chipmunk Card), rather than all possible combinations (Japanese business customer using Chipmunk card). Table 15.4 shows 15 cases that cover all pairwise combinations of calls for the example of Figure 15.15. Table 15.4: A set of test case specifications that cover all pairwise combinations of the possible polymorphic bindings of Account, Credit, and creditCard. Open table as spreadsheet Account
Credit
creditCard
USAccount
EduCredit
VISACard
USAccount
BizCredit
AmExpCard
USAccount
individualCredit
ChipmunkCard
UKAccount
EduCredit
AmExpCard
UKAccount
BizCredit
VISACard
UKAccount
individualCredit
ChipmunkCard
EUAccount
EduCredit
ChipmunkCard
EUAccount
BizCredit
AmExpCard
EUAccount
individualCredit
VISACard
JPAccount
EduCredit
VISACard
JPAccount
BizCredit
ChipmunkCard
JPAccount
individualCredit
AmExpCard
OtherAccount
EduCredit
ChipmunkCard
OtherAccount
BizCredit
VISACard
OtherAccount
individualCredit
AmExpCard
The combinations in Table 15.4 were of dynamic bindings in a single call. Bindings in a sequence of calls can also interact. Consider, for example, method getYTDPurchased of class Account shown in Figure 15.4 on page 278, which computes the total yearly purchase associated with one account to determine the applicable discount. Chipmunk offers tiered discounts to customers whose total yearly purchase reaches a threshold, considering all subsidiary accounts. The total yearly purchase for an account is computed by method getYTDPurchased, which sums purchases by all customers using the account and all subsidiaries. Amounts are always recorded in the local currency of the account, but getYTDPurchased sums the purchases of subsidiaries even when they use different currencies (e.g., when some are bound to subclass USAccount and others to EUAccount). The intra-and interclass testing techniques presented in the previous section may fail to reveal this type of fault. The problem can be addressed by selecting test cases that cover combinations of polymorphic calls and bindings. To identify sequential combinations of bindings, we must first identify individual polymorphic calls and binding sets, and then select possible sequences. Let us consider for simplicity only the method getYTDPurchased. This method is called once for each customer and once for each subsidiary of the account and in both cases can be dynamically bound to methods belonging to any of the subclasses of Account (UKAccount, EUAccount, and so on). At each of these calls, variable totalPurchased is used and changed, and at the end of the method it is used twice more (to set an instance variable and to return a value from the method). Data flow analysis may be used to identify potential interactions between possible bindings at a point where a variable is modified and points where the same value is used. Any of the
standard data flow testing criteria could be extended to consider each possible method binding at the point of definition and the point of use. For instance, a single definition-use pair becomes n × m pairs if the point of definition can be bound in n ways and the point of use can be bound in m ways. If this is impractical, a weaker but still useful alternative is to vary both bindings independently, which results in m or n pairs (whichever is greater) rather than their product. Note that this weaker criterion would be very likely to reveal the fault in getYTDPurchased, provided the choices of binding at each point are really independent rather than going through the same set of choices in lockstep. In many cases, binding sets are not mutually independent, so the selection of combinations is limited.
15.10 Inheritance Inheritance does not introduce new classes of faults except insofar as it is associated with polymorphism and dynamic binding, which we have already discussed, and exception handling, which is discussed in Section 15.12. It does provide an opportunity for optimization by reusing test cases and even test executions. Subclasses share methods with ancestors. Identifying which methods do not need to be retested and which test cases can be reused may significantly reduce testing effort. Methods of a subclass can be categorized as New if they are newly defined in the subclass – that is, they do not occur in the ancestor. New methods include those with the same name but different parameters than methods in ancestor classes. Recursive if they are inherited from the ancestor without change – that is, they occur only in the ancestor. Redefined if they are overridden in the subclass, that is, both occur in the subclass. Abstract new if they are newly defined and abstract in the subclass. Abstract recursive if they are inherited from the ancestor, where they are abstract. Abstract redefined if they are redefined in the subclass, and they are abstract in the ancestor. When testing a base class, one that does not specialize a previously tested class, we can summarize the testing information in a simple table that indicates the sets of generated and executed test cases. Such a table is called a testing history. In general we will have four sets of test cases for a method: intraclass functional, intraclass structural, interclass functional, and interclass structural. For methods that do not call methods in other classes, we will have only intraclass test cases, since no integration test focuses on such methods. For abstract methods, we will only have functional test cases, since we do not have the code of the method. Each set of test cases is marked with a flag that indicates whether the test set can be executed. Table 15.5 shows a testing history for class LineItem, whose code is shown in Figure 15.16. Methods validItem, getWeightGm, getHeightCm, getWidthCm, and getDepthCm are abstract and do not interact with external classes; thus we only have intraclass functional test cases that cannot be directly executed. Method getUnitPrice is abstract, but from the specifications (not shown here) we can infer that it interacts with other classes; thus we have both intra-and interclass functional test cases. Both the constructor and method getExtendedPrice are implemented and interact with other classes (Order and AccountType, respectively), and thus we have all four sets of test cases. Table 15.5: Testing history for class LineItem
Table 15.5: Testing history for class LineItem Open table as spreadsheet Method
Intra funct
Intra struct
Inter funct
Inter struct
LineItem
〈TSLI1,Y 〉
〈TSLI2,Y 〉
〈TSLI3,Y 〉
〈TSLI4,Y 〉
validItem
〈TSvI1,N〉
〈–,–〉
〈–,–〉
〈–,–〉
getUnitPrice
〈TSgUP1,N〉
〈–,–〉
〈TSgUP3,N〉
〈–,–〉
getExtendedPrice
〈TSgXP1,Y 〉
〈TSgXP2,Y 〉
〈TSgXP3,Y 〉
〈TSgXP4,Y 〉
getWeightGm
〈TSgWG1,N〉
〈–,–〉
〈–,–〉
〈–,–〉
getHeightCm
〈TSgHC1,N〉
〈–,–〉
〈–,–〉
〈–,–〉
getWidthCm
〈TSgWC1,N〉
〈–,–〉
〈–,–〉
〈–,–〉
getDepthCm
〈TSgDC1,N〉
〈–,–〉
〈–,–〉
〈–,–〉
Legend: 〈TSI,B〉 refers to test set I, to be executed if B = Y. 〈–,–〉 means no applicable tests. 1
/** One line item of a customer order (abstract). */ 2 public abstract class LineItem { 3 4 /** The order this LineItem belongs to. */ 5 protected Order order; 6 7 /** Constructor links item to owning order. Must call in sub 8 public LineItem(Order order) { order = order; } 9 10 /** Stock-keeping unit (sku) is unique key to all product da 11 public String sku; 12 13 /** Number of identical units to be purchased. */ 14 public int units=1; 15 16 /** Has this line item passed all validation tests? */ 17 public abstract boolean validItem(); 18 19 /** Price of a single item. */ 20 public abstract int getUnitPrice(AccountType accountType); 21 22 /** Extended price for number of units */ 23 public int getExtendedPrice(AccountType accountType)
24 25 26 27 28 29 30 31 32 33 34 35
{
return units * this.getUnitPrice(accountType); }
// Dimensions for packing and shipping (required of all top/** Weight in grams */ public abstract int getWeightGm(); /** Height in centimeters */ public abstract int getHeightCm(); /** Width in Centimeters. */ public abstract int getWidthCm(); /** Depth in Centimeters */ public abstract int getDepthCm(); }
Figure 15.16: Part of a Java implementation of the abstract class LineItem. New and abstract new methods need to be tested from scratch, thus we need to derive the needed test cases and execute them. We report the testing activity in the testing history of the new class by adding a new row and new test cases. Recursive and abstract recursive methods do not need to be retested. Thus the old test sets are copied into the new table and marked as not-to-be-executed. Redefined and abstract redefined methods must be retested, so we add new test cases and mark them to be executed. Table 15.6 shows the testing history for class CompositeItem that specializes class LineItem. The code of class CompositeItem is shown in Figure 15.17. Class CompositeItem adds a constructor, and thus we add a line to the testing history that indicates the four sets of test cases to be added and executed. It redefines method getUnitPrice, which was virtual in class LineItem: the functional test cases derived for class LineItem are thus executed, and new structural test cases are added. All other classes are inherited, and thus the testing history reports all test cases and marks them as not-to-be-executed. Table 15.6: Testing history for class CompositeItem. New test sets are marked with a prime. Open table as spreadsheet Method
Intra funct
Intra struct
Inter funct
Inter struct
LineItem
〈TSLI1,N〉
〈TSLI2,N〉
〈TSLI3,N〉
〈TSLI4,N〉
validItem
〈TSvI1,N〉
〈–,–〉
〈–,–〉
〈–,–〉
getUnitPrice
〈TSgUP1,Y 〉
〈TS′gUP2,Y 〉
〈TSgUP3,Y 〉
〈TS′gUP4,Y 〉
getExtendedPrice
〈TSgXP1,N〉
〈TSgXP2,N〉
〈TSgXP3,N〉
〈TSgXP4,N〉
getWeightGm
〈TSgWG1,N〉
〈–,–〉
〈–,–〉
〈–,–〉
getHeightCm
〈TSgHC1,N〉
〈–,–〉
〈–,–〉
〈–,–〉
getWidthCm
〈TSgWC1,N〉
〈–,–〉
〈–,–〉
〈–,–〉
getDepthCm
〈TSgDC1,N〉
〈–,–〉
〈–,–〉
〈–,–〉
CompositeItem
〈TS′CM1,Y 〉
〈TS′CM2,Y 〉
〈TS′CM3,Y 〉
〈TS′CM4,Y 〉
package Orders; 2 import Accounts.AccountType; 3 import Prices.Pricelist; 4 import java.util.*; 5 6 /** 7 * A composite line item includes a “wrapper” item for the whole 8 * bundle and a set of zero or more component items. 9 */ 10 public abstract class CompositeItem extends LineItem { 11 12 /** 13 * A composite item has some unifying name and base price 14 * (which might be zero) and has zero or more additional pa 15 * which are themselves line items. 16 */ 17 private Vector parts = new Vector(); 18 19 /** 20 * Constructor from LineItem, links to an encompassing Orde 21 */ 22 public CompositeItem(Order order) { 23 super( order); 24 } 25 26 public int getUnitPrice(AccountType accountType) { 27 Pricelist prices = new Pricelist(); 28 int price = prices.getPrice(sku, accountType); 29 for (Enumeration e = parts.elements(); e.hasMoreElements 30 { 31 LineItem i = (LineItem) e.nextElement(); 32 price += i.getUnitPrice(accountType); 33 } 34 return price; 35 } 36 } 1
37 Figure 15.17: Part of a Java implementation of class CompositeItem. The testing history approach reduces the number of tests to be executed, but requires extra effort of keeping track of testing activities. Effort is repaid mainly when it is possible to avoid designing new test cases, but when the cost of executing test cases is high (e.g., because the test requires interaction with an external device or a human) the savings in test execution cost can also be significant. If the cost of executing test cases is negligible, it may be cheaper to simply retest all classes regardless of the tests executed on the ancestors.
15.11 Genericity Generics, also known as parameterized types or (in C++) as templates, are an important tool for building reusable components and libraries. A generic class (say, linked lists) is designed to be instantiated with many different parameter types (e.g., LinkedList and LinkedList). We can test only instantiations, not the generic class itself, and we may not know in advance all the different ways a generic class might be instantiated. A generic class is typically designed to behave consistently over some set of permitted parameter types. Therefore the testing (and analysis) job can be broken into two parts: showing that some instantiation is correct and showing that all permitted instantiations behave identically. Testing a single instantiation raises no particular problems, provided we have source code for both the generic class and the parameter class. Roughly speaking, we can design test cases as if the parameter were copied textually into the body of the generic class. Consider first the case of a generic class that does not make method calls on, nor access fields of, its parameters. Ascertaining this property is best done by inspecting the source code, not by testing it. If we can nonetheless conjecture some ways in which the generic and its parameter might interact (e.g., if the generic makes use of some service that a parameter type might also make use of, directly or indirectly), then we should design test cases aimed specifically at detecting such interaction. Gaining confidence in an unknowable set of potential instantiations becomes more difficult when the generic class does interact with the parameter class. For example, Java (since version 1.5) has permitted a declaration like this: class PriorityQueue { … } The generic PriorityQueue class will be able to make calls on the methods of interface Comparable. Now the behavior of PriorityQueue is not independent of E, but it should be dependent only in certain very circumscribed ways, and in particular it should behave correctly whenever E obeys the requirements of the contract implied by Comparable. The contract imposed on permitted parameters is a kind of specification, and specificationbased (functional) test selection techniques are an appropriate way to select representative instantiations of the generic class. For example, if we read the interface specification for java.lang.Comparable, we learn that most but not all classes that implement Comparable also satisfy the rule (x.compareTo(y) == 0) == (x.equals(y)) Explicit mention of this condition strongly suggests that test cases should include instantiations with classes that do obey this rule (class String, for example) and others that do not (e.g., class BigDecimal with two BigDecimal values 4.0 and 4.00).
15.12 Exceptions Programs in modern object-oriented languages use exceptions to separate handling of error cases from the primary program logic, thereby simplifying normal control flow. Exceptions also greatly reduce a common class of faults in languages without exception-handling constructs. One of the most common faults in C programs, for example, is neglecting to check for the error indications returned by a C function. In a language like Java, an exception is certain to interrupt normal control flow. The price of separating exception handling from the primary control flow logic is introduction of implicit control flows. The point at which an exception is caught and handled may be far from the point at which it is thrown. Moreover, the association of exceptions with handlers is dynamic. In most object-oriented languages and procedural languages that provide exception handling, an exception propagates up the stack of calling methods until it reaches a matching handler. Since exceptions introduce a kind of control flow, one might expect that it could be treated like other control flow in constructing program models and deriving test cases. However, treating every possible exception this way would create an unwieldy control flow graph accounting for potential exceptions at every array subscript reference, every memory allocation, every cast, and so on, and these would be multiplied by matching them to every handler that could appear immediately above them on the call stack. Worse, many of these potential exceptions are actually impossible, so the burden would not be just in designing test cases for each of them but in deciding which can actually occur. It is more practical to consider exceptions separately from normal control flow in test design. We can dismiss from consideration exceptions triggered by program errors signaled by the underlying system (subscript errors, bad casts, etc.), since exercising these exceptions adds nothing to other efforts to prevent or find the errors themselves. If a method A throws an exception that indicates a programming error, we can take almost the same approach. However, if there are exception handlers for these program error exceptions, such as we may find in fault-tolerant programs or in libraries that attempt to maintain data consistency despite errors in client code, then it is necessary to test the error recovery code (usually by executing it together with a stub class with the programming error). This is different and much less involved than testing the error recovery code coupled with every potential point at which the error might be present in actual code. Exceptions that indicate abnormal cases but not necessarily program errors (e.g., exhaustion of memory or premature end-of-file) require special treatment. If the handler for these is local (e.g., a Java try block with an exception handler around a group of file operations), then the exception handler itself requires testing. Whether to test each individual point where exceptions bound to the same handler might be raised (e.g., each individual file operation within the same try block) is a matter of judgment. The remaining exceptions are those that are allowed to propagate beyond the local context in which they are thrown. For example, suppose method A makes a call to method B, within
a Java try block with an exception handler for exceptions of class E. Suppose B has no exception handler for E and makes a call to method C, which throws E. Now the exception will propagate up the chain of method calls until it reaches the handler in A. There could be many such chains, which depend in part on overriding inherited methods, and it is difficult (sometimes even impossible) to determine all and only the possible pairings of points where an exception is thrown with handlers in other methods. Since testing all chains through which exceptions can propagate is impractical, it is best to make it unnecessary. A reasonable design rule to enforce is that, if a method propagates an exception without catching it, the method call should have no other effect. If it is not possible to ensure that method execution interrupted by an exception has no effect, then an exception handler should be present (even if it propagates the same exception by throwing it again). Then, it should suffice to design test cases to exercise each point at which an exception is explicitly thrown by application code, and each handler in application code, but not necessarily all their combinations.
Open Research Issues Many problems involved in test and analysis of object-oriented systems are still open. Most results about functional testing refer to a subset of UML and to algebraic specifications. Additional work is needed to complete the available methods to cope with all aspects of object-oriented systems and different specification approaches. The few techniques for structural testing disclose a wide set of problems that need additional investigation. We need additional experimental data about the effectiveness of the available techniques and better ways to cope with interclass testing. Test and analysis problems of many features that characterize object-oriented systems, such as exceptions, polymorphism, dynamic binding, and inheritance, have been investigated only partially and need additional work. Despite a good deal of experience with objectoriented design, we still have little information about common faults, and we lack fault taxonomies.
Further Reading Many recent books on software testing and software engineering address object-oriented software to at least some degree. The most complete book-length account of current methods is Binder’s Testing Object Oriented Systems [Bin00]. Structural state-based testing is discussed in detail by Buy, Orso, and Pezz` e [BOP00]. The data flow approach to testing software with polymorphism and dynamic binding was initially proposed by Orso [Ors98]. Harrold, McGregor, and Fitzpatrick [HMF92] provide a detailed discussion of the use of testing histories for selecting test cases for subclasses. Th´ evenod-Fosse and Waeselynck describe statistical testing using statechart specifications [TFW93]. An excellent paper by Doong and Frankl [DF94] introduces
equivalent scenarios. Although Doong and Frankl discuss their application with algebraic specifications (which are not much used in practice), the value of the approach does not hinge on that detail.
Related Topics Basic functional and structural testing strategies are treated briefly here, and readers who have not already read Chapters 10, 11, and 12 will find there a more thorough presentation of the rationale and basic techniques for those approaches. Chapters 13 and 14 likewise present the basic data flow and model-based testing approaches in more detail. As integration testing progresses beyond small clusters of classes to major subsystems and components, the interclass testing techniques described in this chapter will become less relevant, and component testing techniques presented in Chapter 21 more important. The system and acceptance testing techniques described in Chapter 22 are as appropriate to object-oriented software as they are to mixed and purely procedural software systems.
Exercises The set of test cases given in Table 15.1 is not the smallest test suite that satisfies the transition coverage criterion for the finite state machine (FSM) of Figure 15.7. 1. Derive a smaller set of test cases that satisfy the transition coverage criterion for the FSM. 15.1 2. Compare the two sets of test cases. What are the advantages of each?
15.2
15.3
15.4
3. Derive a suite of test cases that satisfies the simple transition coverage criterion but does not satisfy the transition coverage criterion. The test cases given in Table 15.1 assume that transitions not given explicitly are “don’t care,” and thus we do not exercise them. Modify the test suite, first assuming that omitted transitions are “error” transitions. Next, modify the same test suite, but instead assuming that the omitted transitions are “self” transitions. Are the two modified test suites different? Why or why not? Generate at least one equivalent and one nonequivalent scenario for at least one of the test cases TCA,…,TCE of Table 15.1. A canonical representation is a unique representation of a set of equivalent objects. For example, {a,a,c,b}, {c,b,a}, and {a,b,c} are all representations of the same mathematical set object. If we choose a lexicographically sorted representation without duplicates as a canonical representation, then we will use {a,b,c} as the unique way of writing that set. Imagine we are using the equivalent scenarios approach to test a hash table class. Why might we want a toString method that returns a canonical representation of the table? Give an example of a test case in which you might use it.
Chapter 16: Fault-Based Testing A model of potential program faults is a valuable source of information for evaluating and designing test suites. Some fault knowledge is commonly used in functional and structural testing, for example when identifying singleton and error values for parameter characteristics in category-partition testing or when populating catalogs with erroneous values, but a fault model can also be used more directly. Fault-based testing uses a fault model directly to hypothesize potential faults in a program under test, as well as to create or evaluate test suites based on its efficacy in detecting those hypothetical faults.
Required Background Chapter 9 The introduction to test case selection and adequacy sets the context for this chapter. Though not strictly required, it is helpful in understanding how the techniques described in this chapter should be applied. Chapter 12 Some basic knowledge of structural testing criteria is required to understand the comparison of fault-based with structural testing criteria.
16.1 Overview Engineers study failures to understand how to prevent similar failures in the future. For example, failure of the Tacoma Narrows Bridge in 1940 led to new understanding of oscillation in high wind and to the introduction of analyses to predict and prevent such destructive oscillation in subsequent bridge design. The causes of an airline crash are likewise extensively studied, and when traced to a structural failure they frequently result in a directive to apply diagnostic tests to all aircraft considered potentially vulnerable to similar failures. Experience with common software faults sometimes leads to improvements in design methods and programming languages. For example, the main purpose of automatic memory management in Java is not to spare the programmer the trouble of releasing unused memory, but to prevent the programmer from making the kind of memory management errors (dangling pointers, redundant deallocations, and memory leaks) that frequently occur in C and C++ programs. Automatic array bounds checking cannot prevent a programmer from using an index expression outside array bounds, but can make it much less likely that the fault escapes detection in testing, as well as limiting the damage incurred if it does lead to operational failure (eliminating, in particular, the buffer overflow attack as a means of subverting privileged programs). Type checking reliably detects many other faults during program translation. Of course, not all programmer errors fall into classes that can be prevented or statically detected using better programming languages. Some faults must be detected through testing, and there too we can use knowledge about common faults to be more effective. The basic concept of fault-based testing is to select test cases that would distinguish the program under test from alternative programs that contain hypothetical faults. This is usually approached by modifying the program under test to actually produce the hypothetical faulty programs. Fault seeding can be used to evaluate the thoroughness of a test suite (that is, as an element of a test adequacy criterion), or for selecting test cases to augment a test suite, or to estimate the number of faults in a program.
16.2 Assumptions in Fault-Based Testing The effectiveness of fault-based testing depends on the quality of the fault model and on some basic assumptions about the relation of the seeded faults to faults that might actually be present. In practice, the seeded faults are small syntactic changes, like replacing one variable reference by another in an expression, or changing a comparison from 0) { emit(buf, pos); } }
Figure 16.1: Program transduce converts line endings among Unix, DOS, and Macintosh conventions. The main procedure, which selects the output line end convention, and the output procedure emit are not shown. Since mutants must be valid, mutation operators are syntactic patterns defined relative to particular programming languages. Figure 16.2 shows some mutation operators for the C language. Constraints are associated with mutation operators to guide selection of test cases likely to distinguish mutants from the original program. For example, the mutation operator svr (scalar variable replacement) can be applied only to variables of compatible type (to be valid), and a test case that distinguishes the mutant from the original program must execute the modified statement in a state in which the original variable and its substitute have different values. Open table as spreadsheet ID
Operator
Description
Constraint
Operand Modifications constant for constant replacement
replace constant C1 with constant C2 C1 ≠ C2
scr
scalar for constant replacement
replace constant C with scalar variable C≠X X
acr
array for constant replacement
replace constant C with array reference A[I]
crp
scr
struct for constant replacement
C ≠ A[I]
replace constant C with struct field S
C≠S
svr
scalar variable replacement
replace scalar variable X with a scalar variable Y
X≠Y
csr
constant for scalar variable replacement
replace scalar variable X with a constant C
X≠C
asr
array for scalar variable replacement
replace scalar variable X with an array reference A[I]
X ≠ A[I]
struct for scalar replacement
replace scalar variable X with struct field S
X≠S
ssr
vie
car
scalar variable initialization elimination constant for array replacement
sar
scalar for array replacement
cnr
comparable array replacement
sar
struct for array reference replacement
remove initialization of a scalar variable
replace array reference A[I] with constant C replace array reference A[I] with scalar variable X
A[I]≠C
A[I]≠C
replace array reference with a comparable array reference replace array reference A[I] with a struct field S
A[I]≠S
Expression Modifications abs
absolute value insertion
replace e by abs(e)
e= BUFLEN–2) (pos == BUFLEN–2)
1U 1D
2U
2D
2M End Long Mixed
–
–
–
–
–
–
–
–
Mj
ror
32
(pos > 0) (pos >= 0)
–
x
x
x
x
–
–
–
Mk
sdl
16
atCR = 0 nothing
–
–
–
–
–
–
–
–
16
atCR = 0 pos = 0
–
–
–
–
–
–
–
x
Ml
ssr
Open table as spreadsheet Test case
1U 1D
Description
Test case
One line, Unix lineend
2M
One line, DOS lineend
End
Description
Two lines, Mac line-end Last line not terminated with line-end sequence
2U Two lines, Unix line-end Long 2D
Two lines, DOS line-end
Mixed
Very long line (greater than buffer length) Mix of DOS and Unix line ends in the same file
Figure 16.3: A sample set of mutants for program Transduce generated with mutation operators from Figure 16.2. x indicates the mutant is killed by the test case in the column head. kills Mj, which can be distinguished from the original program by test cases 1D,2U, 2D, and 2M. Mutants Mi, Mk, and Ml are not distinguished from the original program by any test in TS. We say that mutants not killed by a test suite are live. A mutant can remain live for two reasons: The mutant can be distinguished from the original program, but the test suite T does not contain a test case that distinguishes them (i.e., the test suite is not adequate with respect to the mutant). The mutant cannot be distinguished from the original program by any test case (i.e., the mutant is equivalent to the original program). Given a set of mutants SM and a test suite T, the fraction of nonequivalent mutants killed by T measures the adequacy of T with respect to SM. Unfortunately, the problem of identifying equivalent mutants is undecidable in general, and we could err either by claiming that a mutant is equivalent to the program under test when it is not or by counting some equivalent mutants among the remaining live mutants. The adequacy of the test suite TS evaluated with respect to the four mutants of Figure 16.3 is 25%. However, we can easily observe that mutant Mi is equivalent to the original program (i.e., no input would distinguish it). Conversely, mutants Mk and Ml seem to be nonequivalent to the original program: There should be at least one test case that distinguishes each of them from the original program. Thus the adequacy of TS, measured after eliminating the equivalent mutant Mi, is 33%. Mutant Ml is killed by test case Mixed, which represents the unusual case of an input file containing both DOS-and Unix-terminated lines. We would expect that Mixed would also kill Mk, but this does not actually happen: Both Mk and the original program produce the same result for Mixed. This happens because both the mutant and the original program fail in the same way.[1] The use of a simple oracle for checking the correctness of the outputs (e.g., checking each output against an expected output) would reveal the fault. The test suite TS2 obtained by adding test case Mixed to TS would be 100% adequate (relative to this set of mutants) after removing the fault.
Mutation Analysis vs. Structural Testing For typical sets of syntactic mutants, a mutation-adequate test suite will also be adequate with respect to simple structural criteria such as statement or branch coverage. Mutation adequacy can simulate and subsume a structural coverage criterion if the set of mutants can be killed only by satisfying the corresponding test coverage obligations. Statement coverage can be simulated by applying the mutation operator sdl (statement deletion) to each statement of a program. To kill a mutant whose only difference from the program under test is the absence of statement S requires executing the mutant and the program under test with a test case that executes S in the original program. Thus to kill all mutants generated by applying the operator sdl to statements of the program under test, we need a test suite that causes the execution of each statement in the original program. Branch coverage can be simulated by applying the operator cpr (constant for predicate replacement) to all predicates of the program under test with constants True and False. To kill a mutant that differs from the program under test for a predicate P set to the constant value False, we need to execute the mutant and the program under test with a test case that causes the execution of the True branch of P. To kill a mutant that differs from the program under test for a predicate P set to the constant value True,we need to execute the mutant and the program under test with a test case that causes the execution of the False branch of P. A test suite that satisfies a structural test adequacy criterion may or may not kill all the corresponding mutants. For example, a test suite that satisfies the statement coverage adequacy criterion might not kill an sdl mutant if the value computed at the statement does not affect the behavior of the program on some possible executions.
[1]The
program was in regular use by one of the authors and was believed to be correct. Discovery of the fault came as a surprise while using it as an example for this chapter.
16.5 Variations on Mutation Analysis The mutation analysis process described in the preceding sections, which kills mutants based on the outputs produced by execution of test cases, is known as strong mutation. It can generate a number of mutants quadratic in the size of the program. Each mutant must be compiled and executed with each test case until it is killed. The time and space required for compiling all mutants and for executing all test cases for each mutant may be impractical. The computational effort required for mutation analysis can be reduced by decreasing the number of mutants generated and the number of test cases to be executed. Weak mutation analysis decreases the number of tests to be executed by killing mutants when they produce a different intermediate state, rather than waiting for a difference in the final result or observable program behavior. weak mutation analysis With weak mutation, a single program can be seeded with many faults. A “metamutant” program is divided into segments containing original and mutated source code, with a mechanism to select which segments to execute. Two copies of the metamutant are executed in tandem, one with only original program code selected and the other with a set of live mutants selected. Execution is paused after each segment to compare the program state of the two versions. If the state is equivalent, execution resumes with the next segment of original and mutated code. If the state differs, the mutant is marked as dead, and execution of original and mutated code is restarted with a new selection of live mutants. Weak mutation testing does not decrease the number of program mutants that must be considered, but it does decrease the number of test executions and compilations. This performance benefit has a cost in accuracy: Weak mutation analysis may “kill” a mutant even if the changed intermediate state would not have an effect on the final output or observable behavior of the program. Like structural test adequacy criteria, mutation analysis can be used either to judge the thoroughness of a test suite or to guide selection of additional test cases. If one is designing test cases to kill particular mutants, then it may be important to have a complete set of mutants generated by a set of mutation operators. If, on the other hand, the goal is a statistical estimate of the extent to which a test suite distinguishes programs with seeded faults from the original program, then only a much smaller statistical sample of mutants is required. Aside from its limitation to assessment rather than creation statistical mutation analysis of test suites, the main limitation of statistical mutation analysis is that partial coverage is meaningful only to the extent that the generated mutants are a valid statistical model of occurrence frequencies of actual faults. To avoid reliance on this implausible assumption, the target coverage should be 100% of the sample; statistical sampling may keep the sample small enough to permit careful examination of equivalent mutants. Estimating Population Sizes
Counting fish Lake Winnemunchie is inhabited by two kinds of fish, a native trout and an introduced species of chub. The Fish and Wildlife Service wishes to estimate the populations to evaluate their efforts to eradicate the chub without harming the population of native trout. The population of chub can be estimated statistically as follows. 1000 chub are netted, their dorsal fins are marked by attaching a tag, then they are released back into the lake. Over the next weeks, fishermen are asked to report the number of tagged and untagged chub caught. If 50 tagged chub and 300 untagged chub are caught, we can calculate
and thus there are about 6000 untagged chub remaining in the lake. It may be tempting to also ask fishermen to report the number of trout caught and to perform a similar calculation to estimate the ratio between chub and trout. However, this is valid only if trout and chub are equally easy to catch, or if one can adjust the ratio using a known model of trout and chub vulnerability to fishing. Counting residual faults A similar procedure can be used to estimate the number of faults in a program: Seed a given number S of faults in the program. Test the program with some test suite and count the number of revealed faults. Measure the number of seeded faults detected, DS, and also the number of natural faults DN detected. Estimate the total number of faults remaining in the program, assuming the test suite is as effective at finding natural faults as it is at finding seeded faults, using the formula
If we estimate the number of faults remaining in a program by determining the proportion of seeded faults detected, we must be wary of the pitfall of estimating trout population by counting chub. The seeded faults are chub, the real faults are trout, and we must either have good reason for believing the seeded faults are no easier to detect than real remaining faults, or else make adequate allowances for uncertainty. The difference is that we cannot avoid the problem by repeating the process with trout – once a fault has been detected, our knowledge of its presence cannot be erased. We depend, therefore, on a very good fault model, so that the chub are as representative as possible of trout. Of course, if we use special bait for chub, or design test cases to detect particular seeded faults, then statistical estimation of the total population of fish or errors cannot be justified.
Hardware Fault-based Testing Fault-based testing is widely used for semiconductor and hardware system validation and evaluation both for evaluating the quality of test suites and for evaluating fault tolerance.
Semiconductor testing has conventionally been aimed at detecting random errors in fabrication, rather than design faults. Relatively simple fault models have been developed for testing semiconductor memory devices, the prototypical faults being “stuck-at-0” and “stuckat-1” (a gate, cell, or pin that produces the same logical value regardless of inputs). A number of more complex fault models have been developed for particular kinds of semiconductor devices (e.g., failures of simultaneous access in dualport memories). A test vector (analogous to a test suite for software) can be judged by the number of hypothetical faults it can detect, as a fraction of all possible faults under the model. Fabrication of a semiconductor device, or assembly of a hardware system, is more analogous to copying disk images than to programming. The closest analog of software is not the hardware device itself, but its design – in fact, a high-level design of a semiconductor device is essentially a program in a language that is compiled into silicon. Test and analysis of logic device designs faces the same problems as test and analysis of software, including the challenge of devising fault models. Hardware design verification also faces the added problem that it is much more expensive to replace faulty devices that have been delivered to customers than to deliver software patches. In evaluation of fault tolerance in hardware, the usual approach is to modify the state or behavior rather than the system under test. Due to a difference in terminology between hardware and software testing, the corruption of state or modification of behavior is called a “fault,” and artificially introducing it is called “fault injection.” Pin-level fault injection consists of forcing a stuck-at-0, a stuck-at-1, or an intermediate voltage level (a level that is neither a logical 0 nor a logical 1) on a pin of a semiconductor device. Heavy ion radiation is also used to inject random faults in a running system. A third approach, growing in importance as hardware complexity increases, uses software to modify the state of a running system or to simulate faults in a running simulation of hardware logic design.
Fault seeding can be used statistically in another way: To estimate the number of faults remaining in a program. Usually we know only the number of faults that have been detected, and not the number that remains. However, again to the extent that the fault model is a valid statistical model of actual fault occurrence, we can estimate that the ratio of actual faults found to those still remaining should be similar to the ratio of seeded faults found to those still remaining. Once again, the necessary assumptions are troubling, and one would be unwise to place too much confidence in an estimate of remaining faults. Nonetheless, a prediction with known weaknesses is better than a seat-of-the-pants guess, and a set of estimates derived in different ways is probably the best one can hope for. While the focus of this chapter is on fault-based testing of software, related techniques can be applied to whole systems (hardware and software together) to evaluate fault tolerance. Some aspects of fault-based testing of hardware are discussed in the sidebar on page 323.
Open Research Issues Fault-based testing has yet to be widely applied in software development, although it is an important research tool for evaluating other test selection techniques. Its limited impact on software practice so far can be blamed perhaps partly on computational expense and partly on the lack of adequate support by industrial strength tools. One promising direction in fault-based testing is development of fault models for particular classes of faults. These could result in more sharply focused fault-based techniques, and also partly address concerns about the extent to which the fault models conventionally used in mutation testing are representative of real faults. Two areas in which researchers have attempted to develop focused models, expressed as sets of mutation operators, are component interfaces and concurrency constructs. Particularly important is development of fault models based on actual, observed faults in software. These are almost certainly dependent on application domain and perhaps to some extent also vary across software development organizations, but too little empirical evidence is available on the degree of variability.
Further Reading Software testing using fault seeding was developed by Hamlet [Ham77] and independently by DeMillo, Lipton, and Sayward [DLS78]. Underlying theories for fault-based testing, and in particular on the conditions under which a test case can distinguish faulty and correct versions of a program, were developed by Morell [Mor90] and extended by Thompson, Richardson, and Clarke [TRC93]. Statistical mutation using a Bayesian approach to grow the sample until sufficient evidence has been collected has been described by Sahinoglu and Spafford [SS90]. Weak mutation was proposed by Howden [How82]. The sample mutation operators used in this chapter are adapted from the Mothra software testing environment [DGK+88].
Exercises
Consider the C function in Figure 16.4, used to determine whether a misspelled word a dictionary word by at most one character, which may be a deletion, an insertion, or (e.g., “text” is edit distance 1 from “test” by a substitution, and edit distance 1 from “t deletion of “s”).
1 2 3 4 5
/* edit1( s1, s2 ) returns TRUE iff s1 can be transformed * by inserting, deleting, or substituting a single charact * by a no-op (i.e., if they are already equal). */ 6 int edit1( char *s1, char *s2) {
16.1
7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
if (*s1 == 0) { if (*s2 == 0) return TRUE; /* Try inserting a character in s1 or deleting in s2 * if (*(s2+1)==0) return TRUE; return FALSE; } if (*s2 == 0) { /* Only match is by deleting last char f if (*(s1 + 1) == 0) return TRUE; return FALSE; } /* Now we know that neither string is empty */ if (*s1 == *s2) { return edit1(s1 +1, s2 +1); }
/* Mismatch; only dist 1 possibilities are identical str * inserting, deleting, or substituting character */
/* Substitution: We “look past” the mismatched character if (strcmp(s1+1, s2+1) == 0) return TRUE; /* Deletion: look past character in s1 */ if (strcmp(s1+1, s2) == 0) return TRUE; /* Insertion: look past character in s2 */ if (strcmp(s1, s2+1) == 0) return TRUE; return FALSE; }
Figure 16.4: C function to determine whether one string is with distance 1 of another.
Suppose we seed a fault in line 27, replacing s1 +1 by s1 + 0. Is there a test case th mutant using weak mutation, but not using strong mutation? Display such a test case one, or explain why there is none.
16.2
16.3
We have described weak mutation as continuing execution up to the point that a muta then restarting execution of the original and mutated program from the beginning. Wh execution just continue after killing a mutant? What would be necessary to make cont execution possible?
Motivate the need for the competent programmer and the coupling effect hypotheses mutation analysis still make sense if these hypotheses did not hold? Why?
16.4
Generate some invalid, valid-but-not-useful, useful, equivalent and nonequivalent muta program in Figure 16.1 using mutant operators from Figure
Chapter 17: Test Execution Whereas test design, even when supported by tools, requires insight and ingenuity in similar measure to other facets of software design, test execution must be sufficiently automated for frequent reexecution without little human involvement. This chapter describes approaches for creating the run-time support for generating and managing test data, creating scaffolding for test execution, and automatically distinguishing between correct and incorrect test case executions.
Required Background Chapter 7 Reasoning about program correctness is closely related to test oracles that recognize incorrect behavior at run-time. Chapters 9 and 10 Basic concepts introduced in these chapters are essential background for understanding the distinction between designing a test case specification and executing a test case. Chapters 11 through 16 These chapters provide more context and concrete examples for understanding the material presented here.
17.1 Overview Designing tests is creative; executing them should be as mechanical as compiling the latest version of the product, and indeed a product build is not complete until it has passed a suite of test cases. In many organizations, a complete build-and-test cycle occurs nightly, with a report of success or problems ready each morning. The purpose of run-time support for testing is to enable frequent hands-free reexecution of a test suite. A large suite of test data may be generated automatically from a more compact and abstract set of test case specifications. For unit and integration testing, and sometimes for system testing as well, the software under test may be combined with additional “scaffolding” code to provide a suitable test environment, which might, for example, include simulations of other software and hardware resources. Executing a large number of test cases is of little use unless the observed behaviors are classified as passing or failing. The human eye is a slow, expensive, and unreliable instrument for judging test outcomes, so test scaffolding typically includes automated test oracles. The test environment often includes additional support for selecting test cases (e.g., rotating nightly through portions of a large test suite over the course of a week) and for summarizing and reporting results.
17.2 From Test Case Specifications to Test Cases If the test case specifications produced in test design already include concrete input values and expected results, as for example in the category-partition method, then producing a complete test case may be as simple as filling a template with those values. A more general test case specification (e.g., one that calls for “a sorted sequence, length greater than 2, with items in ascending order with no duplicates”) may designate many possible concrete test cases, and it may be desirable to generate just one instance or many. There is no clear, sharp line between test case design and test case generation. A rule of thumb is that, while test case design involves judgment and creativity, test case generation should be a mechanical step. Automatic generation of concrete test cases from more abstract test case specifications reduces the impact of small interface changes in the course of development. Corresponding changes to the test suite are still required with each program change, but changes to test case specifications are likely to be smaller and more localized than changes to the concrete test cases. Instantiating test cases that satisfy several constraints may be simple if the constraints are independent (e.g., a constraint on each of several input parameter values), but becomes more difficult to automate when multiple constraints apply to the same item. Some wellformed sets of constraints have no solution at all (“an even, positive integer that is not the sum of two primes”). Constraints that appear to be independent may not be. For example, a test case specification that constrains both program input and output imposes a conjunction of two constraints on output (it conforms to the given output constraint and it is produced by the given input). General test case specifications that may require considerable computation to produce test data often arise in model-based testing. For example, if a test case calls for program execution corresponding to a certain traversal of transitions in a finite state machine model, the test data must trigger that traversal, which may be quite complex if the model includes computations and semantic constraints (e.g., a protocol model in Promela; see Chapter 8). Fortunately, model-based testing is closely tied to model analysis techniques that can be adapted as test data generation methods. For example, finite state verification techniques typically have facilities for generating counter-examples to asserted properties. If one can express the negation of a test case specification, then treating it as a property to be verified will result in a counter-example from which a concrete test case can be generated.
17.3 Scaffolding During much of development, only a portion of the full system is available for testing. In modern development methodologies, the partially developed system is likely to consist of one or more runnable programs and may even be considered a version or prototype of the final system from very early in construction, so it is possible at least to execute each new portion of the software as it is constructed, but the external interfaces of the evolving system may not be ideal for testing; often additional code must be added. For example, even if the actual subsystem for placing an order with a supplier is available and fully operational, it is probably not desirable to place a thousand supply orders each night as part of an automatic test run. More likely a portion of the order placement software will be “stubbed out” for most test executions. Code developed to facilitate testing is called scaffolding, by analogy to the temporary structures erected around a building during construction or maintenance. Scaffolding may include test drivers (substituting for a main or calling program), test harnesses (substituting for parts of the deployment environment), and stubs (substituting for functionality called or used by the software under test), in addition to program instrumentation and support for recording and managing test execution. A common estimate is that half of the code developed in a software project is scaffolding of some kind, but the amount of scaffolding that must be constructed with a software project can vary widely, and depends both on the application domain and the architectural design and build plan, which can reduce cost by exposing appropriate interfaces and providing necessary functionality in a rational order. The purposes of scaffolding are to provide controllability to execute test cases and observability to judge the outcome of test execution. Sometimes scaffolding is required to simply make a module executable, but even in incremental development with immediate integration of each module, scaffolding for controllability and observability may be required because the external interfaces of the system may not provide sufficient control to drive the module under test through test cases, or sufficient observability of the effect. It may be desirable to substitute a separate test “driver” program for the full system, in order to provide more direct control of an interface or to remove dependence on other subsystems. Consider, for example, an interactive program that is normally driven through a graphical user interface. Assume that each night the program goes through a fully automated and unattended cycle of integration, compilation, and test execution. It is necessary to perform some testing through the interactive interface, but it is neither necessary nor efficient to execute all test cases that way. Small driver programs, independent of the graphical user interface, can drive each module through large test suites in a short time. When testability is considered in software architectural design, it often happens that interfaces exposed for use in scaffolding have other uses. For example, the interfaces needed to drive an interactive program without its graphical user interface are likely to serve also as the interface for a scripting facility. A similar phenomenon appears at a finer grain. For example, introducing a Java interface to isolate the public functionality of a class and hide methods introduced for testing the implementation has a cost, but also potential side
benefits such as making it easier to support multiple implementations of the interface.
17.4 Generic versus Specific Scaffolding The simplest form of scaffolding is a driver program that runs a single, specific test case. If, for example, a test case specification calls for executing method calls in a particular sequence, this is easy to accomplish by writing the code to make the method calls in that sequence. Writing hundreds or thousands of such test-specific drivers, on the other hand, may be cumbersome and a disincentive to thorough testing. At the very least one will want to factor out some of the common driver code into reusable modules. Sometimes it is worthwhile to write more generic test drivers that essentially interpret test case specifications. At least some level of generic scaffolding support can be used across a fairly wide class of applications. Such support typically includes, in addition to a standard interface for executing a set of test cases, basic support for logging test execution and results. Figure 17.1 illustrates use of generic test scaffolding in the JFlex lexical analyzer generator. public final class IntCharSet { 75 … 76 public void add(Interval intervall) { 186 … 187 } 1
1 2 3 4 5 11 12 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
package JFlex.tests; import JFlex.IntCharSet; import JFlex.Interval; import junit.framework.TestCase; … public class CharClassesTest extends TestCase { … public void testAdd1() { IntCharSet set = new IntCharSet(new Interval(‘a’,’h’)); set.add(new Interval(‘o’,’z’)); set.add(new Interval(‘A’,’Z’)); set.add(new Interval(‘h’,’o’)); assertEquals(“{ [‘A’-‘Z’][‘a’-‘z’] }”, set.toString()); } public void testAdd2() { IntCharSet set = new IntCharSet(new Interval(‘a’,’h’)); set.add(new Interval(‘o’,’z’)); set.add(new Interval(‘A’,’Z’)); set.add(new Interval(‘i’,’n’)); assertEquals(“{ [‘A’-‘Z’][‘a’-‘z’] }”, set.toString()); }
99 100
… }
Figure 17.1: Excerpt of JFlex 1.4.1 source code (a widely used open-source scanner generator) and accompanying JUnit test cases. JUnit is typical of basic test scaffolding libraries, providing support for test execution, logging, and simple result checking (assertEquals in the example). The illustrated version of JUnit uses Java reflection to find and execute test case methods; later versions of JUnit use Java annotation (metadata) facilities, and other tools use source code preprocessors or generators. Fully generic scaffolding may suffice for small numbers of hand-written test cases. For larger test suites, and particularly for those that are generated systematically (e.g., using the combinatorial techniques described in Chapter 11 or deriving test case specifications from a model as described in Chapter 14), writing each test case by hand is impractical. Note, however, that the Java code expressing each test case in Figure 17.1 follows a simple pattern, and it would not be difficult to write a small program to convert a large collection of input, output pairs into procedures following the same pattern. A large suite of automatically generated test cases and a smaller set of hand-written test cases can share the same underlying generic test scaffolding. Scaffolding to replace portions of the system is somewhat more demanding, and again both generic and application-specific approaches are possible. The simplest kind of stub, sometimes called a mock, can be generated automatically by analysis of the source code. A mock is limited to checking expected invocations and producing precomputed results that are part of the test case specification or were recorded in a prior execution. Depending on system build order and the relation of unit testing to integration in a particular process, isolating the module under test is sometimes considered an advantage of creating mocks, as compared to depending on other parts of the system that have already been constructed. The balance of quality, scope, and cost for a substantial piece of scaffolding software – say, a network traffic generator for a distributed system or a test harness for a compiler – is essentially similar to the development of any other substantial piece of software, including similar considerations regarding specialization to a single project or investing more effort to construct a component that can be used in several projects. The balance is altered in favor of simplicity and quick construction for the many small pieces of scaffolding that are typically produced during development to support unit and small-scale integration testing. For example, a database query may be replaced by a stub that provides only a fixed set of responses to particular query strings.
17.5 Test Oracles It is little use to execute a test suite automatically if execution results must be manually inspected to apply a pass/fail criterion. Relying on human intervention to judge test outcomes is not merely expensive, but also unreliable. Even the most conscientious and hard-working person cannot maintain the level of attention required to identify one failure in a hundred program executions, little more one or ten thousand. That is a job for a computer. Software that applies a pass/fail criterion to a program execution is called a test oracle, often shortened to oracle. In addition to rapidly classifying a large number of test case executions, automated test oracles make it possible to classify behaviors that exceed human capacity in other ways, such as checking real-time response against latency requirements or dealing with voluminous output data in a machine-readable rather than human-readable form. Ideally, a test oracle would classify every execution of a correct program as passing and would detect every program failure. In practice, the pass/fail criterion is usually imperfect. A test oracle may apply a pass/fail criterion that reflects only part of the actual program specification, or is an approximation, and therefore passes some program executions it ought to fail. Several partial test oracles (perhaps applied with different parts of the test suite) may be more cost-effective than one that is more comprehensive. A test oracle may also give false alarms, failing an execution that it ought to pass. False alarms in test execution are highly undesirable, not only because of the direct expense of manually checking them, but because they make it likely that real failures will be overlooked. Nevertheless sometimes the best we can obtain is an oracle that detects deviations from expectation that may or may not be actual failures. One approach to judging correctness – but not the only one – compares the actual output or behavior of a program with predicted output or behavior. A test case with a comparisonbased oracle relies on predicted output that is either precomputed as part of the test case specification or can be derived in some way independent of the program under test. Precomputing expected test results is reasonable for a small number of relatively simple test cases, and is still preferable to manual inspection of program results because the expense of producing (and debugging) predicted results is incurred once and amortized over many executions of the test case. Support for comparison-based test oracles is often included in a test harness program or testing framework. A harness typically takes two inputs: (1) the input to the program under test (or can be mechanically transformed to a well-formed input), and (2) the predicted output. Frameworks for writing test cases as program code likewise provide support for comparison-based oracles. The assertEquals method of JUnit, illustrated in Figure 17.1, is a simple example of comparison-based oracle support. Comparison-based oracles are useful mainly for small, simple test cases, but sometimes expected outputs can also be produced for complex test cases and large test suites. Capture-replay testing, a special case of this in which the predicted output or behavior is
preserved from an earlier execution, is discussed in this chapter. A related approach is to capture the output of a trusted alternate version of the program under test. For example, one may produce output from a trusted implementation that is for some reason unsuited for production use; it may too slow or may depend on a component that is not available in the production environment. It is not even necessary that the alternative implementation be more reliable than the program under test, as long as it is sufficiently different that the failures of the real and alternate version are likely to be independent, and both are sufficiently reliable that not too much time is wasted determining which one has failed a particular test case on which they disagree.
Figure 17.2: A test harness with a comparison-based test oracle processes test cases consisting of (program input, predicted output) pairs. A third approach to producing complex (input, output) pairs is sometimes possible: It may be easier to produce program input corresponding to a given output than vice versa. For example, it is simpler to scramble a sorted array than to sort a scrambled array. A common misperception is that a test oracle always requires predicted program output to compare to the output produced in a test execution. In fact, it is often possible to judge output or behavior without predicting it. For example, if a program is required to find a bus route from station A to station B, a test oracle need not independently compute the route to ascertain that it is in fact a valid route that starts at A and ends at B. Oracles that check results without reference to a predicted output are often partial, in the sense that they can detect some violations of the actual specification but not others. They check necessary but not sufficient conditions for correctness. For example, if the specification calls for finding the optimum bus route according to some metric, partial oracle a validity check is only a partial oracle because it does not check optimality. Similarly, checking that a sort routine produces sorted output is simple and cheap, but it is only a partial oracle because the output is also required to be a permutation of the input. A cheap partial oracle that can be used for a large number of test cases is often combined with a more expensive comparison-based oracle that can be used with a smaller set of test cases for which predicted output has been obtained. Ideally, a single expression of a specification would serve both as a work assignment and as a source from which useful test oracles were automatically derived. Specifications are often incomplete, and their informality typically makes automatic derivation of test oracles impossible. The idea is nonetheless a powerful one, and wherever formal or semiformal specifications (including design models) are available, it is worth-while to consider whether test oracles can be derived from them. Some of the effort of formalization will be incurred either early, in writing specifications, or later when oracles are derived from them, and
earlier is usually preferable. Model-based testing, in which test cases and test oracles are both derived from design models are discussed in Chapter 14.
17.6 Self-Checks as Oracles A program or module specification describes all correct program behaviors, so an oracle based on a specification need not be paired with a particular test case. Instead, the oracle can be incorporated into the program under test, so that it checks its own work (see Figure 17.3). Typically these self-checks are in the form of assertions, similar to assertions used in symbolic execution and program verification (see Chapter 7), but designed to be checked during execution.
Figure 17.3: When self-checks are embedded in the program, test cases need not include predicted outputs. Self-check assertions may be left in the production version of a system, where they provide much better diagnostic information than the uncontrolled application crash the customer may otherwise report. If this is not acceptable – for instance, if the cost of a runtime assertion check is too high – most tools for assertion processing also provide controls for activating and deactivating assertions. It is generally considered good design practice to make assertions and self-checks be free of side-effects on program state. Side-effect free assertions are essential when assertions may be deactivated, because otherwise suppressing assertion checking can introduce program failures that appear only when one is not testing. Self-checks in the form of assertions embedded in program code are useful primarily for checking module and subsystem-level specifications, rather than overall program behavior. Devising program assertions that correspond in a natural way to specifications (formal or informal) poses two main challenges: bridging the gap between concrete execution values and abstractions used in specification, and dealing in a reasonable way with quantification over collections of values. Test execution necessarily deals with concrete values, while abstract models are indispensable in both formal and informal specifications. Chapter 7 (page 110) describes the role of abstraction functions and structural invariants in specifying concrete operational behavior based on an abstract model of the internal state of a module. The intended effect of an operation is described in terms of a precondition (state before the operation) and postcondition (state after the operation), relating the concrete state to the abstract model. Consider again a specification of the get method of java.util.Map from Chapter 7, with preand postconditions expressed as the Hoare triple
φ is an abstraction function that constructs the abstract model type (sets of key, value pairs) from the concrete data structure. φ is a logical association that need not be implemented when reasoning about program correctness. To create a test oracle, it is useful to have an actual implementation of φ. For this example, we might implement a special observer method that creates a simple textual representation of the set of (key, value) pairs. Assertions used as test oracles can then correspond directly to the specification. Besides simplifying implementation of oracles by implementing this mapping once and using it in several assertions, structuring test oracles to mirror a correctness argument is rewarded when a later change to the program invalidates some part of that argument (e.g., by changing the treatment of duplicates or using a different data structure in the implementation). In addition to an abstraction function, reasoning about the correctness of internal structures usually involves structural invariants, that is, properties of the data structure that are preserved by all operations. Structural invariants are good candidates for self checks implemented as assertions. They pertain directly to the concrete data structure implementation, and can be implemented within the module that encapsulates that data structure. For example, if a dictionary structure is implemented as a red-black tree or an AVL tree, the balance property is an invariant of the structure that can be checked by an assertion within the module. Figure 17.4 illustrates an invariant check found in the source code of the Eclipse programming invariant. 1
package org.eclipse.jdt.internal.ui.text; 2 import java.text.CharacterIterator; 3 import org.eclipse.jface.text.Assert; 4 /** 5 *A CharSequence based implementation of 6 * CharacterIterator. 7 * @since 3.0 8 */ 9 public class SequenceCharacterIterator implements CharacterIter 13 … 14 private void invariant() { 15 Assert.isTrue(fIndex >= fFirst); 16 Assert.isTrue(fIndex last) throw new IllegalArgumentException(); if (last > sequence.length()) throw new IllegalArgumentException(); fSequence= sequence; fFirst= first; fLast= last; fIndex= first; invariant(); } …
public char setIndex(int position) { if (position >= getBeginIndex() && position ctx; 4 SSL_free(inctx->ssl); 5 sslconn->ssl = NULL; 6 inctx->ssl = NULL; 7 inctx->filter_ctx->pssl = NULL; 8 } This memory leak illustrates several properties typical of integration faults. In principle, it stems from incomplete knowledge of the protocol required to interact with some other portion of the code, either because the specification is (inevitably) incomplete or because it is not humanly possible to remember everything. The problem is due at least in part to a weakness of the programming language – it would not have occurred in a language with automatic garbage collection, such as Java. Finally, although the fault would be very difficult to detect with conventional unit testing techniques, there do exist both static and dynamic analysis techniques that could have made early detection much more likely, as discussed in Chapter 18.
Among strategies for incrementally testing partially assembled systems, we can distinguish two main classes: structural and feature oriented. In a structural approach, modules are constructed, assembled, and tested together in an order based on hierarchical structure in the design. Structural approaches include bottom-up, top-down, and a combination sometimes referred to as sandwich or backbone strategy. Feature-oriented strategies derive the order of integration from characteristics of the application, and include threads and critical modules strategies. Top-down and bottom-up strategies are classic alternatives in system construction and
incremental integration testing as modules accumulate. They consist in sorting modules according to the use/include relation (see Chapter 15, page 286), and in starting testing from the top or from the bottom of the hierarchy, respectively. A top-down integration strategy begins at the top of the uses hierarchy, including the interfaces exposed through a user interface or top-level application program interface (API). The need for drivers is reduced or eliminated while descending the hierarchy, since at each stage the already tested modules can be used as drivers while testing the next layer. For example, referring to the excerpt of the Chipmunk Web presence shown in Figure 21.1, we can start by integrating CustomerCare with Customer, while stubbing Account and Order. We could then add either Account or Order and Package, stubbing Model and Component in the last case. We would finally add Model, Slot, and Component in this order, without needing any driver.
Figure 21.1: An excerpt of the class diagram of the Chipmunk Web presence. Modules are sorted from the top to the bottom according to the use/include relation. The topmost modules are not used or included in any other module, while the bottom-most modules do not include or use other modules. Bottom-up integration similarly reduces the need to develop stubs, except for breaking circular relations. Referring again to the example in Figure 21.1, we can start bottom-up by integrating Slot with Component, using drivers for Model and Order.We can then incrementally add Model and Order. We can finally add either Package or Account and Customer, before integrating CustomerCare, without constructing stubs.
Top-down and bottom-up approaches to integration testing can be applied early in the development if paired with similar design strategies: If modules are delivered following the hierarchy, either top-down or bottom-up, they can be integrated and tested as soon as they are delivered, thus providing early feedback to the developers. Both approaches increase controllability and diagnosability, since failures are likely caused by interactions with the newly integrated modules. In practice, software systems are rarely developed strictly top-down or bottom-up. Design and integration strategies are driven by other factors, like reuse of existing modules or commercial off-the-shelf (COTS) components, or the need to develop early prototypes for user feedback. Integration may combine elements of the two approaches, starting from both ends of the hierarchy and proceeding toward the middle. An early top-down approach may result from developing prototypes for early user feedback, while existing modules may be integrated bottom-up. This is known as the sandwich or backbone strategy. For example, referring once more to the small system of Figure 21.1, let us imagine reusing existing modules for Model, Slot, and Component, and developing CustomerCare and Customer as part of an early prototype. We can start integrating CustomerCare and Customer top down, while stubbing Account and Order. Meanwhile, we can integrate bottom-up Model, Slot, and Component with Order, using drivers for Customer and Package. We can then integrate Account with Customer, and Package with Order, before finally integrating the whole prototype system. The price of flexibility and adaptability in the sandwich strategy is complex planning and monitoring. While top-down and bottom-up are straightforward to plan and monitor, a sandwich approach requires extra coordination between development and test. In contrast to structural integration testing strategies, feature-driven strategies select an order of integration that depends on the dynamic collaboration patterns among modules regardless of the static structure of the system. The thread integration testing strategy integrates modules according to system features. Test designers identify threads of execution that correspond to system features, and they incrementally test each thread. The thread integration strategy emphasizes module interplay for specific functionality. Referring to the Chipmunk Web presence, we can identify feature threads for assembling models, finalizing orders, completing payments, packaging and shipping, and so on. Feature thread integration fits well with software processes that emphasize incremental delivery of user-visible functionality. Even when threads do not correspond to usable end-user features, ordering integration by functional threads is a useful tactic to make flaws in integration externally visible. Incremental delivery of usable features is not the only possible consideration in choosing the order in which functional threads are integrated and tested. Risk reduction is also a driving force in many software processes. Critical module integration testing focuses on modules that pose the greatest risk to the project. Modules are sorted and incrementally integrated according to the associated risk factor that characterizes the criticality of each module. Both external risks (such as safety) and project risks (such as schedule) can be considered.
A risk-based approach is particularly appropriate when the development team does not have extensive experience with some aspect of the system under development. Consider once more the Chipmunk Web presence. If Chipmunk has not previously constructed software that interacts directly with shipping services, those interface modules will be critical because of the inherent risks of interacting with externally provided subsystems, which may be inadequately documented or misunderstood and which may also change. Feature-driven test strategies usually require more complex planning and management than structural strategies. Thus, we adopt them only when their advantages exceed the extra management costs. For small systems a structural strategy is usually sufficient, but for large systems feature-driven strategies are usually preferred. Often large projects require combinations of development strategies that do not fit any single test integration strategies. In these cases, quality managers would combine different strategies: top-down, bottom-up, and sandwich strategies for small subsystems, and a blend of threads and critical module strategies at a higher level.
21.3 Testing Components and Assemblies Many software products are constructed, partly or wholly, from assemblies of prebuilt software components.[1] A key characteristic of software components is that the organization that develops a component is distinct from the (several) groups of developers who use it to construct systems. The component developers cannot completely anticipate the uses to which a component will be put, and the system developers have limited knowledge of the component. Testing components (by the component developers) and assemblies (by system developers) therefore brings some challenges and constraints that differ from testing other kinds of module. Reusable components are often more dependable than software developed for a single application. More effort can be invested in improving the quality of a component when the cost is amortized across many applications. Moreover, when reusing a component that has been in use in other applications for some time, one obtains the benefit not only of test and analysis by component developers, but also of actual operational use. The advantages of component reuse for quality are not automatic. They do not apply to code that was developed for a single application and then scavenged for use in another. The benefit of operational experience as a kind of in vivo testing, moreover, is obtained only to the extent that previous uses of the component are quite similar to the new use. These advantages are balanced against two considerable disadvantages. First, a component designed for wide reuse will usually be much more complex than a module designed for a single use; a rule of thumb is that the development effort (including analysis and test) for a widely usable component is at least twice that for a module that provides equivalent functionality for a single application. In addition, a reusable component is by definition developed without full knowledge of the environment in which it will be used, and it is exceptionally difficult to fully and clearly describe all the assumptions, dependencies, and limitations that might impinge upon its use in a particular application. In general, a software component is characterized by a contract or application program interface (API) distinct from its implementation. Where a mature market has developed for components addressing a particular need, a single interface specification (e.g., SQL for database access or document object model (DOM) for access and traversal of XML data) can have several distinct implementations. The contract describes the component by specifying access points of the component, such as procedures (methods) and their parameters, possible exceptions, global variables, and input and output network connections. Even when the interface specification is bound to a single implementation, the logical distinction between interface and implementation is crucial to effective use and testing. Terminology for Components and Frameworks Component A software component is a reusable unit of deployment and composition that is deployed and integrated multiple times and usually by different teams. Components are characterized by a contract or interface and may or may not have state.
Components are often confused with objects, and a component can be encapsulated by an object or a set of objects, but they typically differ in many respects: Components typically use persistent storage, while objects usually have only local state. Components may be accessed by an extensive set of communication mechanisms, while objects are activated through method calls. Components are usually larger grain subsystems than objects. Component contract or interface The component contract describes the access points and parameters of the component, and specifies functional and nonfunctional behavior and any conditions required for using the component. Framework A framework is a micro-architecture or a skeleton of an application, with hooks for attaching application-specific functionality or configuration-specific components. A framework can be seen as a circuit board with empty slots for components. Frameworks and design patterns Patterns are logical design fragments, while frameworks are concrete elements of the application. Frameworks often implement patterns. Component-based system A component-based system is a system built primarily by assembling software components (and perhaps a small amount of application-specific code) connected through a framework or ad hoc “glue code.” COTS The term commercial off-the-shelf, or COTS, indicates components developed for the sale to other organizations.
The interface specification of a component should provide all the information required for reusing the component, including so-called nonfunctional properties such as performance or capacity limits, in addition to functional behavior. All dependence of the component on the environment in which it executes should also be specified. In practice, few component specifications are complete in every detail, and even details that are specified precisely can easily be overlooked or misunderstood when embedded in a complex specification document. The main problem facing test designers in the organization that produces a component is lack of information about the ways in which the component will be used. A component may be reused in many different contexts, including applications for which its functionality is an imperfect fit. A general component will typically provide many more features and options than are used by any particular application. A good deal of functional and structural testing of a component, focused on finding and removing as many program faults as possible, can be oblivious to the context of actual use.
As with system and acceptance testing of complete applications, it is then necessary to move to test suites that are more reflective of actual use. Testing with usage scenarios places a higher priority on finding faults most likely to be encountered in use and is needed to gain confidence that the component will be perceived by its users (that is, by developers who employ it as part of larger systems) as sufficiently dependable. Test designers cannot anticipate all possible uses of a component under test, but they can design test suites for classes of use in the form of scenarios. Test scenarios are closely related to scenarios or use cases in requirements analysis and design. Sometimes different classes of use are clearly evident in the component specification. For example, the W3 Document Object Model (DOM) specification has parts that deal exclusively with HTML markup and parts that deal with XML; these correspond to different uses to which a component implementing the DOM may be put. The DOM specification further provides two “views” of the component interface. In the flat view, all traversal and inspection operations are provided on node objects, without regard to subclass. In the structured view, each subclass of node offers traversal and inspection operations specific to that variety of node. For example, an Element node has methods to get and set attributes, but a Text node (which represents simple textual data within XML or HTML) does not.
Open Research Issues Ensuring quality of components and of component-based systems remains a challenging problem and a topic of current research. One research thread considers how dynamic analysis of components and component-based systems in one environment can produce useful information for assessing likely suitability for using some of the same components in another environment (by characterizing the contexts in which a component has been used successfully). A related approach of characterizing a set of behaviors and recognizing changes or differences (whether or not those differences are failures) may be applicable in the increasingly important context of dynamically configurable and field-upgradable systems, which pose all the problems of component-based systems with the additional complication of performing integration in deployed systems rather than in the development environment. For these and other systems, self-monitoring and postdeployment testing in the field are likely to play an increasingly important role in the future. Software design for testability is an important factor in the cost and effectiveness of test and analysis, particularly for module and component integration. To some extent modelbased testing (Chapter 14) is progress toward producing modules and components with well-specified and testable interfaces, but much remains to be done in characterizing and supporting testability. Design for testability should be an important factor in the evolution of architectural design approaches and notations, including architecture design languages.
Further Reading The buffer overflow problem in libpng, which caused security vulnerabilities in major Windows, Linux, and Mac OS X Web browsers and e-mail clients, was discovered in 2004
and documented by the United States Computer Emergency Readiness Team (CERT) in Vulnerability Note VU#388984 [Uni04]. The full report on the famous Ariane 5 failure [Lio96] is available from several sources on the Web. The NASA report on loss of the Mars Climate Orbiter [Ste99] is also available on the Web. Leveson [Lev04] describes the role of software in the Ariane failure, loss of the Mars Climate Orbiter, and other spacecraft losses. Weyuker [Wey98] describes challenges of testing component-based systems.
Exercises
21.1
21.2
21.3 [1]The
When developing a graphical editor, we used a COTS component for saving and reading files in XML format. During integration testing, the program failed when reading an empty file and when reading a file containing a syntax error. Try to classify the corresponding faults according to the taxonomy described in Table 21.1. The Chipmunk quality team decided to use both thread and critical module integration testing strategies for the Chipmunk Web presence. Envisage at least one situation in which thread integration should be preferred over critical module and one in which critical module testing should be preferred over thread, and motivate the choice. Can a backbone testing strategy yield savings in the cost of producing test scaffolding, relative to other structural integration testing strategies? If so, how and under what conditions? If not, why not?
term component is used loosely and often inconsistently in different contexts. Our working definition and related terms are explained in the sidebar on page 414.
Chapter 22: System, Acceptance, and Regression Testing System testing can be considered a final step in integration testing, but encompassing systemwide properties against a system specification. Acceptance testing abandons specifications in favor of users, and measures how the final system meets users’ expectations. Regression testing checks for faults introduced during evolution.
Required Background Chapter 4 The concepts of dependability, reliability, availability and mean time to failure are important for understanding the difference between system and acceptance testing. Chapter 17 Generating reusable scaffolding and test cases is a foundation for regression testing. Some knowledge about the scaffolding and test case generation problem, though not strictly required, may be useful for understanding regression testing problems.
22.1 Overview System, acceptance, and regression testing are all concerned with the behavior of a software system as a whole, but they differ in purpose. System testing is a check of consistency between the software system and its specification (it is a verification activity). Like unit and integration testing, system testing is primarily aimed at uncovering faults, but unlike testing activities at finer granularity levels, system testing focuses on system-level properties. System testing together with acceptance testing also serves an important role in assessing whether a product can be released to customers, which is distinct from its role in exposing faults to be removed to improve the product. System, Acceptance, and Regression Testing Open table as spreadsheet System test
Acceptance test
Regression test
Checks against requirements specifications
Checks suitability for user needs
Rechecks test cases passed by previous production versions
Performed by development test group
Performed by test group with user involvement
Performed by development test group
Validates usefulness and satisfaction with the product
Verifies correctness and completion of the product
Guards against unintended changes
Flaws in specifications and in development, as well as changes in users’ expectations, may result in products that do not fully meet users’ needs despite passing system tests. Acceptance testing, as its name implies, is a validation activity aimed primarily at the acceptability of the product, and it includes judgments of actual usefulness and usability rather than conformance to a requirements specification. Regression testing is specialized to the problem of efficiently checking for unintended effects of software changes. New functionality and modification of existing code may introduce unexpected interactions and lead latent faults to produce failures not experienced in previous releases.
22.2 System Testing The essential characteristics of system testing are that it is comprehensive, based on a specification of observable behavior, and independent of design and implementation decisions. System testing can be considered the culmination of integration testing, and passing all system tests is tantamount to being complete and free of known bugs. The system test suite may share some test cases with test suites used in integration and even unit testing, particularly when a thread-based or spiral model of development has been taken and subsystem correctness has been tested primarily through externally visible features and behavior. However, the essential characteristic of independence implies that test cases developed in close coordination with design and implementation may be unsuitable. The overlap, if any, should result from using system test cases early, rather than reusing unit and integration test cases in the system test suite. Independence in system testing avoids repeating software design errors in test design. This danger exists to some extent at all stages of development, but always in trade for some advantage in designing effective test cases based on familiarity with the software design and its potential pitfalls. The balance between these considerations shifts at different levels of granularity, and it is essential that independence take priority at some level to obtain a credible assessment of quality. In some organizations, responsibility for test design and execution shifts at a discrete point from the development team to an independent verification and validation team that is organizationally isolated from developers. More often the shift in emphasis is gradual, without a corresponding shift in responsible personnel. Particularly when system test designers are developers or attached to the development team, the most effective way to ensure that the system test suite is not unduly influenced by design decisions is to design most system test cases as early as possible. Even in agile development processes, in which requirements engineering is tightly interwoven with development, it is considered good practice to design test cases for a new feature before implementing the feature. When the time between specifying a feature and implementing it is longer, early design of system tests facilitates risk-driven strategies that expose critical behaviors to system test cases as early as possible, avoiding unpleasant surprises as deployment nears. For example, in the (imaginary) Chipmunk development of Web-based purchasing, some questions were raised during requirements specification regarding the point at which a price change becomes effective. For example, if an item’s catalog price is raised or lowered between the time it is added to the shopping cart and the time of actual purchase, which price is the customer charged? The requirement was clarified and documented with a set of use cases in which outcomes of various interleavings of customer actions and price changes were specified, and each of these scenarios became a system test case specification. Moreover, since this was recognized as a critical property with many opportunities for failure, the system architecture and build-plan for the Chipmunk Web presence was structured with interfaces that could be artificially driven through various scenarios early in
development, and with several of the system test scenarios simulated in earlier integration tests. The appropriate notions of thoroughness in system testing are with respect to the system specification and potential usage scenarios, rather than code or design. Each feature or specified behavior of the system should be accounted for in one or several test cases. In addition to facilitating design for test, designing system test cases together with the system requirements specification document helps expose ambiguity and refine specifications. The set of feature tests passed by the current partial implementation is often used as a gauge of progress. Interpreting a count of failing feature-based system tests is discussed in Chapter 20, Section 20.6. Additional test cases can be devised during development to check for observable symptoms of failures that were not anticipated in the initial system specification. They may also be based on failures observed and reported by actual users, either in acceptance testing or from previous versions of a system. These are in addition to a thorough specification-based test suite, so they do not compromise independence of the quality assessment. Some system properties, including performance properties like latency between an event and system response and reliability properties like mean time between failures, are inherently global. While one certainly should aim to provide estimates of these properties as early as practical, they are vulnerable to unplanned interactions among parts of a complex system and its environment. The importance of such global properties is therefore magnified in system testing. Global properties like performance, security, and safety are difficult to specify precisely and operationally, and they depend not only on many parts of the system under test, but also on its environment and use. For example, U.S. HIPAA regulations governing privacy of medical records require appropriate administrative, technical, and physical safeguards to protect the privacy of health information, further specified as follows: Implementation specification: safeguards. A covered entity must reasonably safeguard protected health information from any intentional or unintentional use or disclosure that is in violation of the standards, implementation specifications or other requirements of this subpart. [Uni00, sec. 164.530(c)(2)] It is unlikely that any precise operational specification can fully capture the HIPAA requirement as it applies to an automated medical records system. One must consider the whole context of use, including, for example, which personnel have access to the system and how unauthorized personnel are prevented from gaining access. Some global properties may be defined operationally, but parameterized by use. For example, a hard-real-time system must meet deadlines, but cannot do so in a completely arbitrary environment; its performance specification is parameterized by event frequency and minimum inter-arrival times. An e-commerce system may be expected to provide a certain level of responsiveness up to a certain number of transactions per second and to
degrade gracefully up to a second rate. A key step is identifying the “operational envelope” of the system, and testing both near the edges of that envelope (to assess compliance with specified goals) and well beyond it (to ensure the system degrades or fails gracefully). Defining borderline and extreme cases is logically part of requirements engineering, but as with precise specification of features, test design often reveals gaps and ambiguities. Not all global properties will be amenable to dynamic testing at all, at least in the conventional sense. One may specify a number of properties that a secure computer system should have, and some of these may be amenable to testing. Others can be addressed only through inspection and analysis techniques, and ultimately one does not trust the security of a system at least until an adversarial team has tried and failed to subvert it. Similarly, there is no set of test cases that can establish software safety, in part because safety is a property of a larger system and environment of which the software is only part. Rather, one must consider the safety of the overall system, and assess aspects of the software that are critical to that overall assessment. Some but not all of those claims may be amenable to testing. Testing global system properties may require extensive simulation of the execution environment. Creating accurate models of the operational environment requires substantial human resources, and executing them can require substantial time and machine resources. Usually this implies that “stress” testing is a separate activity from frequent repetition of feature tests. For example, a large suite of system test cases might well run each night or several times a week, but a substantial stress test to measure robust performance under heavy load might take hours to set up and days or weeks to run. A test case that can be run automatically with few human or machine resources should generally focus on one purpose: to make diagnosis of failed test executions as clear and simple as possible. Stress testing alters this: If a test case takes an hour to set up and a day to run, then one had best glean as much information as possible from its results. This includes monitoring for faults that should, in principle, have been found and eliminated in unit and integration testing, but which become easier to recognize in a stress test (and which, for the same reason, are likely to become visible to users). For example, several embedded system products ranging from laser printers to tablet computers have been shipped with slow memory leaks that became noticeable only after hours or days of continuous use. In the case of the tablet PC whose character recognition module gradually consumed all system memory, one must wonder about the extent of stress testing the software was subjected to. Unit, Integration, and System Testing Open table as spreadsheet Unit Test Test cases derived
module specifications
Integration Test architecture and design specifications
System Test
requirements specification
from Visibility required
all the details of the code
some details of the code, mainly interfaces
Scaffolding required
Potentially complex, to simulate the activation environment (drivers), the modules called by the module under test (stubs) and test oracles
Depends on architecture and integration order. Modules and subsystems can be incrementally integrated to reduce need for drivers and stubs.
Focus on
behavior of individual modules
module integration and interaction
no details of the code
Mostly limited to test oracles, since the whole system should not require additional drivers or stubs to be executed. Sometimes includes a simulated execution environment (e.g., for embedded systems).
system functionality
22.3 Acceptance Testing The purpose of acceptance testing is to guide a decision as to whether the product in its current state should be released. The decision can be based on measures of the product or process. Measures of the product are typically some inference of dependability based on statistical testing. Measures of the process are ultimately based on comparison to experience with previous products. Although system and acceptance testing are closely tied in many organizations, fundamental differences exist between searching for faults and measuring quality. Even when the two activities overlap to some extent, it is essential to be clear about the distinction, in order to avoid drawing unjustified conclusions. Quantitative goals for dependability, including reliability, availability, and mean time between failures, were introduced in Chapter 4. These are essentially statistical measures and depend on a statistically valid approach to drawing a representative sample of test executions from a population of program behaviors. Systematic testing, which includes all of the testing techniques presented heretofore in this book, does not draw statistically representative samples. Their purpose is not to fail at a “typical” rate, but to exhibit as many failures as possible. They are thus unsuitable for statistical testing. The first requirement for valid statistical testing is a precise definition of what is being measured and for what population. If system operation involves transactions, each of which consists of several operations, a failure rate of one operation in a thousand is quite different from a failure rate of one transaction in a thousand. In addition, the failure rate may vary depending on the mix of transaction types, or the failure rate may be higher when one million transactions occur in an hour than when the same transactions are spread across a day. Statistical modeling therefore necessarily involves construction of a model of usage, and the results are relative to that model. Suppose, for example, that a typical session using the Chipmunk Web sales facility consists of 50 interactions, the last of which is a single operation in which the credit card is charged and the order recorded. Suppose the Chipmunk software always operates flawlessly up to the point that a credit card is to be charged, but on half the attempts it charges the wrong amount. What is the reliability of the system? If we count the fraction of individual interactions that are correctly carried out, we conclude that only one operation in 100 fails, so the system is 99% reliable. If we instead count entire sessions, then it is only 50% reliable, since half the sessions result in an improper credit card charge. Statistical models of usage, or operational profiles, may be available from measurement of actual use of prior, similar systems. For example, use of a current telephone handset may be a reasonably good model of how a new handset will be used. Good models may also be obtained in embedded systems whose environment is primarily made up of predictable devices rather than unpredictable humans. In other cases one cannot justify high confidence in a model, but one can limit the uncertainty to a small number of parameters. One can perform sensitivity testing to determine which parameters are critical. Sensitivity testing
consists of repeating statistical tests while systematically varying parameters to note the effect of each parameter on the output. A particular parameter may have little effect on outcomes over the entire range of plausible values, or there may be an effect that varies smoothly over the range. If the effect of a given parameter is either large or varies discontinuously (e.g., performance falls precipitously when system load crosses some threshold), then one may need to make distinct predictions for different value ranges. A second problem faced by statistical testing, particularly for reliability, is that it may take a very great deal of testing to obtain evidence of a sufficient level of reliability. Consider that a system that executes once per second, with a failure rate of one execution in a million, or 99.9999% reliability, fails about 31 times each year; this may require a great testing effort and still not be adequate if each failure could result in death or a lawsuit. For critical systems, one may insist on software failure rates that are an insignificant fraction of total failures. For many other systems, statistical measures of reliability may simply not be worth the trouble. A less formal, but frequently used approach to acceptance testing is testing with users. An early version of the product is delivered to a sample of users who provide feedback on failures and usability. Such tests are often called alpha and beta tests. The two terms distinguish between testing phases. Often the early or alpha phases are performed within the developing organization, while the later or beta phases are performed at users’ sites. In alpha and beta testing, the user sample determines the operational profile. A good sample of users should include representatives of each distinct category of users, grouped by operational profile and significance. Suppose, for example, Chipmunk plans to provide Web-based sales facilities to dealers, industrial customers, and individuals. A good sample should include both users from each of those three categories and a range of usage in each category. In the industrial user category, large customers who frequently issue complex orders as well as small companies who typically order a small number of units should be represented, as the difference in their usage may lead to different failure rates. We may weigh differently the frequency of failure reports from dealers and from direct customers, to reflect either the expected mix of usage in the full population or the difference in consequence of failure.
22.4 Usability A usable product is quickly learned, allows users to work efficiently, and is pleasant to use. Usability involves objective criteria such as the time and number of operations required to perform tasks and the frequency of user error, in addition to the overall, subjective satisfaction of users. For test and analysis, it is useful to distinguish attributes that are uniquely associated with usability from other aspects of software quality (dependability, performance, security, etc.). Other software qualities may be necessary for usability; for example, a program that often fails to satisfy its functional requirements or that presents security holes is likely to suffer poor usability as a consequence. Distinguishing primary usability properties from other software qualities allows responsibility for each class of properties to be allocated to the most appropriate personnel, at the most cost-effective points in the project schedule. Even if usability is largely based on user perception and thus is validated based on user feedback, it can be verified early in the design and through the whole software life cycle. The process of verifying and validating usability includes the following main steps: Inspecting specifications with usability checklists. Inspection provides early feedback on usability. Testing early prototypes with end users to explore their mental model (exploratory test), evaluate alternatives (comparison test), and validate software usability. A prototype for early assessment of usability may not include any functioning software; a cardboard prototype may be as simple as a sequence of static images presented to users by the usability tester. Testing incremental releases with both usability experts and end users to monitor progress and anticipate usability problems. System and acceptance testing that includes expert-based inspection and testing, userbased testing, comparison testing against competitors, and analysis and checks often done automatically, such as a check of link connectivity and verification of browser compatibility. Userbased testing (i.e., testing with representatives of the actual end-user population) is particularly important for validating software usability. It can be applied at different stages, from early prototyping through incremental releases of the final system, and can be used with different goals: exploring the mental model of the user, evaluating design alternatives, and validating against established usability requirements and standards. The purpose of exploratory testing is to investigate the mental model of end users. It consists of asking users about their approach to interactions with the system. For example, during an exploratory test for the Chipmunk Web presence, we may provide users with a generic interface for choosing the model they would like to buy, in order to understand how users will interact with the system. A generic interface could present information about all
laptop computer characteristics uniformly to see which are examined first by the sample users, and thereby to determine the set of characteristics that should belong to the summary in the menu list of laptops. Exploratory test is usually performed early in design, especially when designing a system for a new target population. The purpose of comparison testing is evaluating options. It consists of observing user reactions to alternative interaction patterns. During comparison test we can, for example, provide users with different facilities to assemble the desired Chipmunk laptop configuration, and to identify patterns that facilitate users’ interactions. Comparison test is usually applied when the general interaction patterns are clear and need to be refined. It can substitute for exploratory testing if initial knowledge about target users is sufficient to construct a range of alternatives, or otherwise follows exploratory testing. The purpose of validation testing is assessing overall usability. It includes identifying difficulties and obstacles that users encounter while interacting with the system, as well as measuring characteristics such as error rate and time to perform a task. A well-executed design and organization of usability testing can produce results that are objective and accurately predict usability in the target user population. The usability test design includes selecting suitable representatives of the target users and organizing sessions that guide the test toward interpretable results. A common approach is divided into preparation, execution, and analysis phases. During the preparation phase, test designers define the objectives of the session, identify the items to be tested, select a representative population of end users, and plan the required actions. During execution, users are monitored as they execute the planned actions in a controlled environment. During analysis, results are evaluated, and changes to the software interfaces or new testing sessions are planned, if required. Each phase must be carefully executed to ensure success of the testing session. User time is a valuable and limited resource. Well-focused test objectives should not be too narrow, to avoid useless waste of resources, nor too wide, to avoid scattering resources without obtaining useful data. Focusing on specific interactions is usually more effective than attempting to assess the usability of a whole program at once. For example, the Chipmunk usability test team independently assesses interactions for catalog browsing, order definition and purchase, and repair service. The larger the population sample, the more precise the results, but the cost of very large samples is prohibitive; selecting a small but representative sample is therefore critical. A good practice is to identify homogeneous classes of users and select a set of representatives from each class. Classes of users depend on the kind of application to be tested and may be categorized by role, social characteristics, age, and so on. A typical compromise between cost and accuracy for a well-designed test session is five users from a unique class of homogeneous users, four users from each of two classes, or three users for each of three or more classes. Questionnaires should be prepared for the selected users to verify their membership in their respective classes. Some approaches also assign a weight to each class, according to their importance to the business. For example, Chipmunk
can identify three main classes of users: individual, business, and education customers. Each of the main classes is further divided. Individual customers are distinguished by education level; business customers by role; and academic customers by size of the institution. Altogether, six putatively homogeneous classes are obtained: Individual customers with and without at least a bachelor degree, managers and staff of commercial customers, and customers at small and large education institutions. Users are asked to execute a planned set of actions that are identified as typical uses of the tested feature. For example, the Chipmunk usability assessment team may ask users to configure a product, modify the configuration to take advantage of some special offers, and place an order with overnight delivery. Users should perform tasks independently, without help or influence from the testing staff. User actions are recorded, and comments and impressions are collected with a post-activity questionnaire. Activity monitoring can be very simple, such as recording sequences of mouse clicks to perform each action. More sophisticated monitoring can include recording mouse or eye movements. Timing should also be recorded and may sometimes be used for driving the sessions (e.g., fixing a maximum time for the session or for each set of actions). An important aspect of usability is accessibility to all users, including those with disabilities. Accessibility testing is legally required in some application domains. For example, some governments impose specific accessibility rules for Web applications of public institutions. The set of Web Content Accessibility Guidelines (WCAG) defined by the World Wide Web Consortium are becoming an important standard reference. The WCAG guidelines are summarized in the sidebar on page 426. Web Content Accessibility Guidelines (WCAG)[a] 1. Provide equivalent alternatives to auditory and visual content that convey essentially the same function or purpose. 2. Ensure that text and graphics are understandable when viewed without color. 3. Mark up documents with the proper structural elements, controlling presentation with style sheets rather than presentation elements and attributes. 4. Use markup that facilitates pronunciation or interpretation of abbreviated or foreign text. 5. Ensure that tables have necessary markup to be transformed by accessible browsers and other user agents. 6. Ensure that pages are accessible even when newer technologies are not supported or are turned off. 7. Ensure that moving, blinking, scrolling, or auto-updating objects or pages may be paused or stopped.
8. Ensure that the user interface, including embedded user interface elements, follows principles of accessible design: device-independent access to functionality, keyboard operability, self-voicing, and so on. 9. Use features that enable activation of page elements via a variety of input devices. 10. Use interim accessibility so that assisting technologies and older browsers will operate correctly. 11. Where technologies outside of W3C specifications is used (e.g, Flash), provide alternative versions to ensure accessibility to standard user agents and assistive technologies (e.g., screen readers). 12. Provide context and orientation information to help users understand complex pages or elements. 13. Provide clear and consistent navigation mechanisms to increase the likelihood that a person will find what they are looking for at a site. 14. Ensure that documents are clear and simple, so they may be more easily understood.
[a]Excerpted
and adapted from Web Content Accessibility Guidelines 1.0, W3C Recommendation 5-May 1999; used by permission. The current version is distributed by W3C at http://www.w3.org/TR/WAI-WEBCONTENT.
22.5 Regression Testing When building a new version of a system (e.g., by removing faults, changing or adding functionality, porting the system to a new platform, or extending interoperability), we may also change existing functionality in unintended ways. Sometimes even small changes can produce unforeseen effects that lead to new failures. For example, a guard added to an array to fix an overflow problem may cause a failure when the array is used in other contexts, or porting the software to a new platform may expose a latent fault in creating and modifying temporary files. When a new version of software no longer correctly provides functionality that should be preserved, we say that the new version regresses with respect to former versions. The nonregression of new versions (i.e., preservation of functionality), is a basic quality requirement. Disciplined design and development techniques, including precise specification and modularity that encapsulates independent design decisions, improves the likelihood of achieving nonregression. Testing activities that focus on regression problems are called (non) regression testing. Usually “non” is omitted and we commonly say regression testing. A simple approach to regression testing consists of reexecuting all test cases designed for previous versions. Even this simple retest all approach may present nontrivial problems and costs. Former test cases may not be reexecutable on the new version without modification, and rerunning all test cases may be too expensive and unnecessary. A good quality test suite must be maintained across system versions. Changes in the new software version may impact the format of inputs and outputs, and test cases may not be executable without corresponding changes. Even simple modifications of the data structures, such as the addition of a field or small change of data types, may invalidate former test cases, or outputs comparable with the new ones. Moreover, some test cases may be obsolete, since they test features of the software that have been modified, substituted, or removed from the new version. Scaffolding that interprets test case specifications, rather than fully concrete test data, can reduce the impact of input and output format changes on regression testing, as discussed in Chapter 17. Test case specifications and oracles that capture essential correctness properties, abstracting from arbitrary details of behavior, likewise reduce the likelihood that a large portion of a regression test suite will be invalidated by a minor change. High-quality test suites can be maintained across versions by identifying and removing obsolete test cases, and by revealing and suitably marking redundant test cases. Redundant cases differ from obsolete, being executable but not important with respect to the considered testing criteria. For example, test cases that cover the same path are mutually redundant with respect to structural criteria, while test cases that match the same partition are mutually redundant with respect to functional criteria. Redundant test cases may be introduced in the test suites due to concurrent work of different test designers or to changes in the code. Redundant test cases do not reduce the overall effectiveness of tests, but impact on the cost-benefits trade-off: They are unlikely to reveal faults, but they augment
the costs of test execution and maintenance. Obsolete test cases are removed because they are no longer useful, while redundant test cases are kept because they may become helpful in successive versions of the software. Good test documentation is particularly important. As we will see in Chapter 24, test specifications define the features to be tested, the corresponding test cases, the inputs and expected outputs, as well as the execution conditions for all cases, while reporting documents indicate the results of the test executions, the open faults, and their relation to the test cases. This information is essential for tracking faults and for identifying test cases to be reexecuted after fault removal.
22.6 Regression Test Selection Techniques Even when we can identify and eliminate obsolete test cases, the number of tests to be reexecuted may be large, especially for legacy software. Executing all test cases for large software products may require many hours or days of execution and may depend on scarce resources such as an expensive hardware test harness. For example, some mass market software systems must be tested for compatibility with hundreds of different hardware configurations and thousands of drivers. Many test cases may have been designed to exercise parts of the software that cannot be affected by the changes in the version under test. Test cases designed to check the behavior of the file management system of an operating system is unlikely to provide useful information when reexecuted after changes of the window manager. The cost of reexecuting a test suite can be reduced by selecting a subset of test cases to be reexecuted, omitting irrelevant test cases or prioritizing execution of subsets of the test suite by their relation to changes. Test case prioritization orders frequency of test case execution, executing all of them eventually but reducing the frequency of those deemed least likely to reveal faults by some criterion. Alternate execution is a variant on prioritization for environments with frequent releases and small incremental changes; it selects a subset of regression test cases for each software version. Prioritization can be based on the specification and code-based regression test selection techniques described later in this chapter. In addition, test histories and fault-proneness models can be incorporated in prioritization schemes. For example, a test case that has previously revealed a fault in a module that has recently undergone change would receive a very high priority, while a test case that has never failed (yet) would receive a lower priority, particularly if it primarily concerns a feature that was not the focus of recent changes. Regression test selection techniques are based on either code or specifications. Codebased selection techniques select a test case for execution if it exercises a portion of the code that has been modified. Specification-based criteria select a test case for execution if it is relevant to a portion of the specification that has been changed. Code-based regression test techniques can be supported by relatively simple tools. They work even when specifications are not properly maintained. However, like code-based test techniques in general, they do not scale well from unit testing to integration and system testing. In contrast, specification-based criteria scale well and are easier to apply to changes that cut across several modules. However, they are more challenging to automate and require carefully structured and well-maintained specifications. Among code-based test selection techniques, control-based techniques rely on a record of program elements executed by each test case, which may be gathered from an instrumented version of the program. The structure of the new and old versions of the program are compared, and test cases that exercise added, modified, or deleted elements are selected for reexecution. Different criteria are obtained depending on the program model on which the version comparison is based (e.g., control flow or data flow graph models).
Control flow graph (CFG) regression techniques are based on the differences between the CFGs of the new and old versions of the software. Let us consider, for example, the C function cgi_decode from Chapter 12. Figure 22.1 shows the original function as presented in Chapter 12, while Figure 22.2 shows a revison of the program. We refer to these two versions as 1.0 and 2.0, respectively. Version 2.0 adds code to fix a fault in interpreting hexadecimal sequences ‘%xy’. The fault was revealed by testing version 1.0 with input terminated by an erroneous subsequence ‘%x’, causing version 1.0 to read past the end of the input buffer and possibly overflow the output buffer. Version 2.0 contains a new branch to map the unterminated sequence to a question mark.
1 #include “hex_values.h” 2 /** Translate a string from the CGI encoding to plain ascii text. 3 * ‘+’ becomes space, %xx becomes byte with hex value xx, 4 * other alphanumeric characters map to themselves. 5 * Returns 0 for success, positive for erroneous input 6 * 1 = bad hexadecimal digit 7 */ 8 int cgi_decode(char *encoded, char *decoded) { 9 char *eptr = encoded; 10 char *dptr = decoded; 11 int ok=0; 12 while (*eptr) { 13 char c; 14 c = *eptr; 15 if (c == ‘+’) { /* Case 1: ‘+’ maps to blank */ 16 *dptr = ”; 17 } else if (c == ‘%’) { /* Case 2: ‘%xx’ is hex for character x 18 int digit_high = Hex_Values[*(++eptr)]; /* note illegal => 19 int digit_low = Hex_Values[*(++eptr)]; 20 if ( digit_high == -1 || digit low==-1) { 21 /* dptr=’?’; / 22 ok=1; /* Bad return code */ 23 } else { 24 *dptr = 16* digit_high + digit_low; 25 } 26 } else { /* Case 3: Other characters map to themselves */ 27 *dptr = eptr; 28 } 29 ++dptr; 30 ++eptr; 31 } 32 dptr = ‘
