Paul Ammann
Jeff Offutt
introtest CUUS047-Ammann ISBN 9780521880381 December 6, 2007 2:42 This page intentionally left blank i Char Count=
4,192 406 2MB
Automated Software Testing: Introduction, Management, and Performance
Automated Software Testing Automated Software Testing Introduction, Management, and Performance Elfriede Dustin Jeff R
The HCS12 9S12: An Introduction to Software and Hardware Interfacing
The HCS12/9S12: An Introduction to Software and Hardware Interfacing Second Edition Han-Way Huang Minnesota State Unive
1,578 763 10MB Read more
Psychological Testing and Assessment: An Introduction to Tests & Measurement
Testing Code Security
AU9251_C000.fm Page iii Thursday, May 3, 2007 8:34 AM Maura A. van der Linden Boca Raton New York Auerbach Publicati
Applied Software Measurement
ABOUT THE AUTHOR CAPERS JONES (Narragansett, Rhode Island) is a well-known author, consultant, and speaker in the wor
Software Engineering: Modern Approaches
4,696 2,950 147MB Read more
Forensic Issues in Alcohol Testing
54457_C000.fm Page i Tuesday, September 4, 2007 11:18 AM Half Title Page 54457_C000.fm Page ii Tuesday, September 4,
Software Project Management For Dummies
Introduction to Linear Algebra
introtest
CUUS047-Ammann ISBN 9780521880381
Char Count= 0
Introduction to Software Testing Extensively class tested, this text takes an innovative approach to software testing: it defines testing as the process of applying a few welldefined, general-purpose test criteria to a structure or model of the software. The structure of the text directly reflects the pedagogical approach and incorporates the latest innovations in testing, including modern types of software such as OO, Web applications, and embedded software. The book contains numerous examples throughout. An instructor's solution manual, PowerPoint slides, sample syllabi, additional examples and updates, testing tools for students, and example software programs in Java are available on an extensive Web site at www.introsoftwaretesting.com. Paul Ammann, PhD, is an Associate Professor of software engineering at George Mason University. He received an outstanding teaching award in 2007 from the Volgenau School of Information Technology and Engineering. Dr. Ammann earned an AB degree in computer science from Dartmouth College and MS and PhD degrees in computer science from the University of Virginia. Jeff Offutt, PhD, is a Professor of software engineering at George Mason University. He is editor-in-chief of the Journal of Software Testing, Verification and Reliability; chair of the steering committee for the IEEE International Conference on Software Testing, Verification, and Validation; and on the editorial boards for several journals. He recived the outstanding teacher award from the Volgenau School of Information Technology and Engineering in 2003. Dr. Offutt earned a BS degree in mathematics and data processing from Morehead State University and MS and PhD degrees in computer science from the Georgia Institute of Technology.
Paul Ammann George Mason University
Jeff Offutt George Mason University
Cambridge, New York, Melbourne, Madrid, Cape Town, Singapore, São Paulo Cambridge University Press The Edinburgh Building, Cambridge CB2 8RU, UK Published in the United States of America by Cambridge University Press, New York www.cambridge.org Information on this title: www.cambridge.org/9780521880381 © Paul Ammann and Jeff Offutt 2008 This publication is in copyright. Subject to statutory exception and to the provision of relevant collective licensing agreements, no reproduction of any part may take place without the written permission of Cambridge University Press. First published in print format 2008
eBook (EBL)
Cambridge University Press has no responsibility for the persistence or accuracy of urls for external or third-party internet websites referred to in this publication, and does not guarantee that any content on such websites is, or will remain, accurate or appropriate.
List of Figures List of Tables Preface
Part 1 1
xiii xv
Introduction 1.1 Activities of a Test Engineer 1.1.1 Testing Levels Based on Software Activity 1.1.2 Beizer's Testing Levels Based on Test Process Maturity 1.1.3 Automation of Test Activities 1.2 Software Testing Limitations and Terminology 1.3 Coverage Criteria for Testing 1.3.1 Infeasibility and Subsumption 1.3.2 Characteristics of a Good Coverage Criterion 1.4 Older Software Testing Terminology 1.5 Bibliographic Notes
Coverage Criteria
Graph Coverage 2.1 Overview 2.2 Graph Coverage Criteria 2.2.1 Structural Coverage Criteria 2.2.2 Data Flow Criteria 2.2.3 Subsumption Relationships among Graph Coverage Criteria 2.3 Graph Coverage for Source Code
1 3 4 5 8 10 11 16 20 20 21 22 25 27 27 32 33 44 50 52 v
2.3.1 Structural Graph Coverage for Source Code 2.3.2 Data Flow Graph Coverage for Source Code Graph Coverage for Design Elements 2.4.1 Structural Graph Coverage for Design Elements 2.4.2 Data Flow Graph Coverage for Design Elements Graph Coverage for Specifications 2.5.1 Testing Sequencing Constraints 2.5.2 Testing State Behavior of Software Graph Coverage for Use Cases 2.6.1 Use Case Scenarios Representing Graphs Algebraically 2.7.1 Reducing Graphs to Path Expressions 2.7.2 Applications of Path Expressions 2.7.3 Deriving Test Inputs 2.7.4 Counting Paths in a Flow Graph and Determining Max Path Length 2.7.5 Minimum Number of Paths to Reach All Edges 2.7.6 Complementary Operations Analysis Bibliographic Notes
Logic Coverage 3.1 Overview: Logic Predicates and Clauses 3.2 Logic Expression Coverage Criteria 3.2.1 Active Clause Coverage 3.2.2 Inactive Clause Coverage 3.2.3 Infeasibility and Subsumption 3.2.4 Making a Clause Determine a Predicate 3.2.5 Finding Satisfying Values 3.3 Structural Logic Coverage of Programs 3.3.1 Predicate Transformation Issues 3.4 Specification-Based Logic Coverage 3.5 Logic Coverage of Finite State Machines 3.6 Disjunctive Normal Form Criteria 3.7 Bibliographic Notes Input Space Partitioning 4.1 Input Domain Modeling 4.1.1 Interface-Based Input Domain Modeling 4.1.2 Functionality-Based Input Domain Modeling 4.1.3 Identifying Characteristics 4.1.4 Choosing Blocks and Values 4.1.5 Using More than One Input Domain Model 4.1.6 Checking the Input Domain Model 4.2 Combination Strategies Criteria 4.3 Constraints among Partitions 4.4 Bibliographic Notes
52 54 65 65 67 75 75 77 87 90 91 94 96 96 97 98 98 100 104 104 106 107 111 112 113 115 120 127 131 134 138 147 150 152 153 154 154 156 158 158 160 165 166
Syntax-Based Testing 5.1 Syntax-Based Coverage Criteria 5.1.1 BNF Coverage Criteria 5.1.2 Mutation Testing 5.2 Program-Based Grammars 5.2.1 BNF Grammars for Languages 5.2.2 Program-Based Mutation 5.3 Integration and Object-Oriented Testing 5.3.1 BNF Integration Testing 5.3.2 Integration Mutation 5.4 Specification-Based Grammars 5.4.1 BNF Grammars 5.4.2 Specification-Based Mutation 5.5 Input Space Grammars 5.5.1 BNF Grammars 5.5.2 Mutation for Input Grammars 5.6 Bibliographic Notes
Applying Criteria in Practice
Practical Considerations 6.1 Regression Testing 6.2 Integration and Testing 6.2.1 Stubs and Drivers 6.2.2 Class Integration Test Order 6.3 Test Process 6.3.1 Requirements Analysis and Specification 6.3.2 System and Software Design 6.3.3 Intermediate Design 6.3.4 Detailed Design 6.3.5 Implementation 6.3.6 Integration 6.3.7 System Deployment 6.3.8 Operation and Maintenance 6.3.9 Summary 6.4 Test Plans 6.5 Identifying Correct Outputs 6.5.1 Direct Verification of Outputs 6.5.2 Redundant Computations 6.5.3 Consistency Checks 6.5.4 Data Redundancy 6.6 Bibliographic Notes
215 217 218 218 219 220 221 222 223 223 224 224 224 225 225 230 230 231 231 232 233
Engineering Criteria for Technologies
7.1 Testing Object-Oriented Software 7.1.1 Unique Issues with Testing OO Software
7.1.2 Types of Object-Oriented Faults 7.2 Testing Web Applications and Web Services 7.2.1 Testing Static Hyper Text Web Sites 7.2.2 Testing Dynamic Web Applications 7.2.3 Testing Web Services 7.3 Testing Graphical User Interfaces 7.3.1 Testing GUIs 7.4 Real-Time Software and Embedded Software 7.5 Bibliographic Notes
Building Testing Tools 8.1 Instrumentation for Graph and Logical Expression Criteria 8.1.1 Node and Edge Coverage 8.1.2 Data Flow Coverage 8.1.3 Logic Coverage 8.2 Building Mutation Testing Tools 8.2.1 The Interpretation Approach 8.2.2 The Separate Compilation Approach 8.2.3 The Schema-Based Approach 8.2.4 Using Java Reflection 8.2.5 Implementing a Modern Mutation System 8.3 Bibliographic Notes
Challenges in Testing Software
9.1 Testing for Emergent Properties: Safety and Security 9.1.1 Classes of Test Cases for Emergent Properties 9.2 Software Testability 9.2.1 Testability for Common Technologies 9.3 Test Criteria and the Future of Software Testing 9.3.1 Going Forward with Testing Research 9.4 Bibliographic Notes
List of Criteria
1.1 Activities of test engineers 1.2 Software development activities and testing levels – the "V Model" 2.1 Graph (a) has a single initial node, graph (b) multiple initial nodes, and graph (c) (rejected) with no initial nodes 2.2 Example of paths 2.3 A single entry single exit graph 2.4 Test case mappings to test paths 2.5 A set of test cases and corresponding test paths 2.6 A graph showing node coverage and edge coverage 2.7 Two graphs showing prime path coverage 2.8 Graph with a loop 2.9 Tours, sidetrips, and detours in graph coverage 2.10 An example for prime test paths 2.11 A graph showing variables, def sets and use sets 2.12 A graph showing an example of du-paths 2.13 Graph showing explicit def and use sets 2.14 Example of the differences among the three data flow coverage criteria 2.15 Subsumption relations among graph coverage criteria 2.16 CFG fragment for the if-else structure 2.17 CFG fragment for the if structure without an else 2.18 CFG fragment for the while loop structure 2.19 CFG fragment for the for loop structure 2.20 CFG fragment for the case structure 2.21 TestPat for data flow example 2.22 A simple call graph 2.23 A simple inheritance hierarchy 2.24 An inheritance hierarchy with objects instantiated 2.25 An example of parameter coupling 2.26 Coupling du-pairs 2.27 Last-defs and first-uses
page 4 6 28 29 30 31 32 34 37 37 38 40 44 46 47 49 50 52 53 53 54 54 56 65 66 67 68 69 69 ix
2.28 2.29 2.30 2.31 2.32 2.33 2.34 2.35 2.36 2.37 2.38 2.39 2.40 2.41 2.42 2.43 2.44 2.45 2.46 2.47 2.48 2.49 2.50 2.51 2.52 2.53 2.54 2.55 2.56 3.1 3.2 3.3 3.4 3.5 3.6 4.1 4.2 5.1 5.2 5.3 5.4 5.5 5.6 5.7 5.8 5.9
Quadratic root program Def-use pairs under intra-procedural and inter-procedural data flow Def-use pairs in object-oriented software Def-use pairs in web applications and other distributed software Control flow graph using the File ADT Elevator door open transition Stutter – Part A Stutter – Part B A FSM representing Stutter, based on control flow graphs of the methods A FSM representing Stutter, based on the structure of the software A FSM representing Stutter, based on modeling state variables A FSM representing Stutter, based on the specifications Class Queue for exercises. ATM actor and use cases Activity graph for ATM withdraw funds Examples of path products Null path that leads to additive identity φ A or lambda Example graph to show reduction to path expressions After step 1 in path expression reduction After step 2 in path expression reduction After step 3 in path expression reduction Removing arbitrary nodes Eliminating node n2 Removing sequential edges Removing self-loop edges Final graph with one path expression Graph example for computing maximum number of paths Graph example for complementary path analysis Subsumption relations among logic coverage criteria TriTyp – Part A TriTyp – Part B Calendar method FSM for a memory car seat – Lexus 2003 ES300 Fault detection relationships Partitioning of input domain D into three blocks Subsumption relations among input space partitioning criteria Method Min and six mutants Mutation testing process Partial truth table for (a ∧ b) Finite state machine for SMV specification Mutated finite state machine for SMV specification Finite state machine for bank example Finite state machine for bank example grammar Simple XML message for books XML schema for books
71 72 72 73 76 79 80 81 82 83 84 85 86 88 90 92 93 94 94 95 95 95 95 95 95 96 96 97 99 113 121 122 132 135 143 151 163 177 181 187 199 200 202 202 204 205
7.1 7.2 7.3 7.4 7.5 7.6 7.7 7.8 7.9 7.10 7.11 7.12 7.13 7.14 8.2 8.3 8.4 8.5
Example class hierarchy in UML Data flow anomalies with polymorphism Calls to d() when object has various actual types ITU: Descendant with no overriding methods SDA, SDIH: State definition anomalies IISD: Example of indirect inconsistent state definition ACB1: Example of anomalous construction behavior SVA: State visibility anomaly Sample class hierarchy (a) and associated type families (b) Control flow graph fragment (a) and associated definitions and uses (b) Def-use pairs in object-oriented software Control flow schematic for prototypical coupling sequence Sample class hierarchy and def-use table Coupling sequence: o of type A (a) bound to instance of A (b), B (c) or C (d) Node coverage instrumentation Edge coverage instrumentation All uses coverage instrumentation Correlated active clause coverage instrumentation
2.1 2.2 2.3 2.4 2.5 3.1 3.2 3.3 3.4 3.5 3.6 3.7 3.8 4.1 4.2 4.3 4.4 4.5 4.6 4.7 5.1 6.1 6.2 6.3 6.4 6.5 6.6 6.7 6.8 7.1
Defs and uses at each node in the CFG for TestPat page 57 Defs and uses at each edge in the CFG for TestPat 57 Du-path sets for each variable in TestPat 58 Test paths to satisfy all du-paths coverage on TestPat 59 Test paths and du-paths covered on TestPat 59 Reachability for Triang predicates 123 Reachability for Triang predicates – reduced by solving for triOut 124 Predicate coverage for Triang 125 Clause coverage for Triang 126 Correlated active clause coverage for Triang 127 Correlated active clause coverage for cal() preconditions 133 Predicates from memory seat example 136 DNF fault classes 143 First partitioning of TriTyp's inputs (interface-based) 156 Second partitioning of TriTyp's inputs (interface-based) 157 Possible values for blocks in the second partitioning in Table 4.2 157 Geometric partitioning of TriTyp's inputs (functionality-based) 158 Correct geometric partitioning of TriTyp's inputs (functionality-based) 158 Possible values for blocks in geometric partitioning in Table 4.5 159 Examples of invalid block combinations 165 Java's access levels 193 Testing objectives and activities during requirements analysis and specification 221 Testing objectives and activities during system and software design 222 Testing objectives and activities during intermediate design 222 Testing objectives and activities during detailed design 223 Testing objectives and activities during implementation 223 Testing objectives and activities during integration 224 Testing objectives and activities during system deployment 224 Testing objectives and activities during operation and maintenance 225 Faults and anomalies due to inheritance and polymorphism 240 xiii
ITU: Code example showing inconsistent type usage IC: Incomplete construction of state variable fd Summary of sample coupling paths Binding triples for coupling sequence from class hierarchy in Figure 7.13
This book presents software testing as a practical engineering activity, essential to producing high-quality software. It is designed to be used as the primary textbook in either an undergraduate or graduate course on software testing, as a supplement to a general course on software engineering or data structures, and as a resource for software test engineers and developers. This book has a number of unique features:
It organizes the complex and confusing landscape of test coverage criteria with
a novel and extremely simple structure. At a technical level, software testing is based on satisfying coverage criteria. The book's central observation is that there are few truly different coverage criteria, each of which fits easily into one of four categories: graphs, logical expressions, input space, and syntax structures. This not only simplifies testing, but it also allows a convenient and direct theoretical treatment of each category. This approach contrasts strongly with the traditional view of testing, which treats testing at each phase in the development process differently. It is designed and written to be a textbook. The writing style is direct, it builds the concepts from the ground up with a minimum of required background, and it includes lots of examples, homework problems, and teaching materials. It provides a balance of theory and practical application, presenting testing as a collection of objective, quantitative activities that can be measured and repeated. The theoretical concepts are presented when needed to support the practical activities that test engineers follow. It assumes that testing is part of a mental discipline that helps all IT professionals develop higher-quality software. Testing is not an anti-engineering activity, and it is not an inherently destructive process. Neither is it only for testing specialists or domain experts who know little about programming or math. It is designed with modular, interconnecting pieces; thus it can be used in multiple courses. Most of the book requires only basic discrete math and introductory programming, and the parts that need more background are clearly marked. By
using the appropriate sections, this book can support several classes, as described later in the preface. It assumes the reader is learning to be an engineer whose goal is to produce the best possible software with the lowest possible cost. The concepts in this book are well grounded in theory, are practical, and most are currently in use.
WHY SHOULD THIS BOOK BE USED? Not very long ago, software development companies could afford to employ programmers who could not test and testers who could not program. For most of the industry, it was not necessary for either group to know the technical principles behind software testing or even software development. Software testing in industry historically has been a nontechnical activity. Industry viewed testing primarily from the managerial and process perspective and had limited expectations of practitioners' technical training. As the software engineering profession matures, and as software becomes more pervasive in everyday life, there are increasingly stringent requirements for software reliability, maintainability, and security. Industry must respond to these changes by, among other things, improving the way software is tested. This requires increased technical expertise on the part of test engineers, as well as increased emphasis on testing by software developers. The good news is that the knowledge and technology are available and based on over 30 years of research and practice. This book puts that knowledge into a form that students, test engineers, test managers, and developers can access. At the same time, it is relatively rare to find courses that teach testing in universities. Only a few undergraduate courses exist, almost no masters degree programs in computer science or software engineering require a course in software testing, and only a few dozen have an elective course. Not only is testing not covered as an essential part of undergraduate computer science education, most computer science students either never gain any knowledge about testing, or see only a few lectures as part of a general course in software engineering. The authors of this book have been teaching software testing to software engineering and computer science students for more than 15 years. Over that time we somewhat reluctantly came to the conclusion that no one was going to write the book we wanted to use. Rather, to get the book we wanted, we would have to write it. Previous testing books have presented software testing as a relatively simple subject that relies more on process than technical understanding of how software is constructed, as a complicated and fractured subject that requires detailed understanding of numerous software development technologies, or as a completely theoretical subject that can be mastered only by mathematicians and theoretical computer scientists. Most books on software testing are organized around the phases in a typical software development lifecycle, an approach that has the unfortunate side effect of obscuring common testing themes. Finally, most testing books are written as reference books, not textbooks. As a result, only instructors with prior expertise in software testing can easily teach the subject. This book is accessible to instructors who are not already testing experts.
This book differs from other books on software testing in other important ways. Many books address managing the testing process. While this is important, it is equally important to give testers specific techniques grounded in basic theory. This book provides a balance of theory and practical application. This is important information that software companies must have; however, this book focuses specifically on the technical nuts-and-bolts issues of designing and creating tests. Other testing books currently on the market focus on specific techniques or activities, such as system testing or unit testing. This book is intended to be comprehensive over the entire software development process and to cover as many techniques as possible. As stated previously, the motivation for this book is to support courses in software testing. Our first target was our own software testing course in our Software Engineering MS program at George Mason University. This popular elective is taught to about 30 computer science and software engineering students every semester. We also teach PhD seminars in software testing, industry short courses on specialized aspects, and lectures on software testing in various undergraduate courses. Although few undergraduate courses on software testing exist, we believe that they should exist, and we expect they will in the near future. Most testing books are not designed for classroom use. We specifically wrote this book to support our classroom activities, and it is no accident that the syllabus for our testing course, available on the book's Web site (www.introsoftwaretesting.com), closely follows the table of contents for this book. This book includes numerous carefully worked examples to help students and teachers alike learn the sometimes complicated concepts. The instructor's resources include high-quality powerpoint slides, presentation hints, solutions to exercises, and working software. Our philosophy is that we are doing more than writing a book; we are offering our course to the community. One of our goals was to write material that is scholarly and true to the published research literature, but that is also accessible to nonresearchers. Although the presentation in the book is quite a bit different from the research papers that the material is derived from, the essential ideas are true to the literature. To make the text flow more smoothly, we have removed the references from the presentation. For those interested in the research genealogy, each chapter closes with a bibliographic notes section that summarizes where the concepts come from.
WHO SHOULD READ THIS BOOK? Students who read and use this book will learn the fundamental principles behind software testing, and how to apply these principles to produce better software, faster. They will not only become better programmers, they will also be prepared to carry out high-quality testing activities for their future employers. Instructors will be able to use this book in the classroom, even without prior practical expertise in software testing. The numerous exercises and thought-provoking problems, classroom-ready and classroom-tested slides, and suggested outside activities make this material teachable by instructors who are not already experts in software testing. Research students such as beginning PhD students will find this book to be an invaluable resource as a starting point to the field. The theory is sound and clearly
presented, the practical applications reveal what is useful and what is not, and the advanced reading and bibliographic notes provide pointers into the literature. Although the set of research students in software testing is a relatively small audience, we believe it is a key audience, because a common, easily achievable baseline would reduce the effort required for research students to join the community of testing researchers. Researchers who are already familiar with the field will find the criteria-approach to be novel and interesting. Some may disagree with the pedagogical approach, but we have found that the view that testing is an application of only a few criteria to a very few software structures to be very helpful to our research. We hope that testing research in the future will draw away from searches for more criteria to novel uses and evaluations of existing criteria. Testers in the industry will find this book to be an invaluable collection of techniques that will help improve their testing, no matter what their current process is. The criteria presented here are intended to be used as a "toolbox" of tricks that can be used to find faults. Developers who read this book will find numerous ways to improve their own software. Their self-testing activities can become more efficient and effective, and the discussions of software faults that test engineers search for will help developers avoid them. To paraphrase a famous parable, if you want to teach a person to be a better fisherman, explain how and where the fish swim. Finally, managers will find this book to be a useful explanation of how clever test engineers do their job, and of how test tools work. They will be able to make more effective decisions regarding hiring, promotions, and purchasing tools.
HOW CAN THIS BOOK BE USED? A major advantage of the structure of this book is that it can be easily used for several different courses. Most of the book depends on material that is taught very early in college and some high schools: basic concepts from data structures and discrete math. The sections are organized so that the early material in each chapter is accessible to less advanced students, and material that requires more advanced knowledge is clearly marked. Specifically, the book defines six separate sets of chapter sections that form streams through the book: 1. A module within a CS II course 2. A sophomore-level course on software testing 3. A module in a general software engineering course 4. A senior-level course on software testing 5. A first-year MS level course on software testing 6. An advanced graduate research-oriented course on software testing 7. Industry practioner relevant sections The stream approach is illustrated in the abbreviated table of contents in the figure shown on pp. xix–xx. Each chapter section is marked with which stream it belongs too. Of course, individual instructors, students, and readers may prefer to adapt the stream to their own interests or purposes. We suggest that the first two sections of Chapter 1 and the first two sections of Chapter 6 are appropriate reading for a module in a data structures (CS II) class, to be followed by a simple
Preface Stream 1: Module in a CS II course. Stream 2: Sophomore-level course on software testing. Stream 3: Module in a general software engineering course. Stream 4: Senior-level course on software testing. Stream 5: First-year MS course on software testing. Stream 6: Advanced graduate research-oriented course on software testing. Stream 7: Industry practitioner relevant sections STREAMS 1
Part I: Overview Chapter 1. Introduction 1.1 Activities of a Test Engineer 1.2 Software Testing Limitations and Terminology 1.3 Coverage Criteria for Testing 1.4 Older Software Testing Terminology 1.5 Bibliographic Notes
Part II: Coverage Criteria Chapter 2. Graph Coverage 2.1 Overview 2.2 Graph Coverage Criteria 2.3 Graph Coverage for Source Code 2.4 Graph Coverage for Design Elements 2.5 Graph Coverage for Specifications 2.6 Graph Coverage for Use Cases 2.7 Representing Graphs Algebraically 2.8 Bibliographic Notes
Chapter 3. Logic Coverage 3.1 Overview: Logic Predicates and Clauses 3.2 Logic Expression Coverage Criteria 3.3 Structural Logic Coverage of Programs 3.4 Specification-Based Logic Coverage 3.5 Logic Coverage of Finite State Machines 3.6 Disjunctive Normal Form Criteria 3.7 Bibliographic Notes
Chapter 4. Input Space Partitioning 4.1 Input Domain Modeling 4.2 Combination Strategies Criteria 4.3 Constraints among Partitions 4.4 Bibliographic Notes
Chapter 5. Syntax-Based Testing 5.1 Syntax-Based Coverage Criteria 5.2 Program-Based Grammars 5.3 Integration and Object-Oriented Testing 5.4 Specification-Based Grammars 5.5 Input Space Grammars 5.6 Bibliographic Notes
Part III: Applying Criteria in Practice Chapter 6. Practical Considerations 6.1 Regression Testing 6.2 Integration and Testing 6.3 Test Process 6.4 Test Plans 6.5 Identifying Correct Outputs 6.5 Bibliographic Notes
Chapter 7. Engineering Criteria for Technologies 7.1 Testing Object-Oriented Software 7.2 Testing Web Applications and Web Services 7.3 Testing Graphical User Interfaces 7.4 Real-Time Software and Embedded Software 7.5 Bibliographic Notes
Chapter 8. Building Testing Tools 8.1 Instrumentation for Graph and Logical Expression Criteria 8.2 Building Mutation Testing Tools 8.3 Bibliographic Notes
Chapter 9. Challenges in Testing Software 9.1 Testing for Emergent Properties: Safety and Security 9.2 Software Testability 9.3 Test Criteria and the Future of Software Testing 9.4 Bibliographic Notes
assignment. Our favorite is to ask the students to retrieve one of their previously graded programs and satisfy some simple test criterion like branch coverage. We offer points for every fault found, driving home two concepts: an "A" grade doesn't mean the program always works, and finding faults is a good thing. The sophomore-level course on software testing (stream 2) is designed to immediately follow a data structures course (CS II). The marked sections contain material that depends only on data structures and discrete math. A module in a general software engineering course (stream 3) could augment the survey material typical in such courses. The sections marked provide basic literacy in software testing. The senior-level course on software testing (stream 4) is the primary target for this text. It adds material that requires a little more sophistication in terms of
software development than the sophomore stream. This includes sections in Chapter 2 on data flow testing, sections that involve integration testing of multiple modules, and sections that rely on grammars or finite state machines. Most senior computer science students will have seen this material in their other courses. Most of the sections that appear in stream 4 but not stream 2 could be added to stream 2 with appropriate short introductions. It is important to note that a test engineer does not need to know all the theory of parsing to use data flow testing or all the theory on finite state machines to use statecharts for testing. The graduate-level course on software testing (stream 5) adds some additional sections that rely on a broader context and that require more theoretical maturity. For example, these sections use knowledge of elementary formal methods, polymorphism, and some of the UML diagrams. Some of the more advanced topics and the entire chapter on building testing tools are also intended for a graduate audience. This chapter could form the basis for a good project, for example, to implement a simple coverage analyzer. An advanced graduate course in software testing with a research emphasis such as a PhD seminar (stream 6) includes issues that are still unproven and research in nature. The bibliographic notes are recommended only for these students as indicators for future in-depth reading. Finally, sections that are reasonably widely used in industry, especially those that have commercial tool support, are marked for stream 7. These sections have a minimum of theory and omit criteria that are still of questionable usefulness. Extensive supplementary materials, including sample syllabuses, PowerPoint slides, presentation hints, solutions to exercises, working software, and errata are available on the book's companion Web site.
ACKNOWLEDGMENTS Many people helped us write this book. Not only have the students in our Software Testing classes at George Mason been remarkably tolerant of using a work in progress, they have enthusiastically provided feedback on how to improve the text. We cannot acknowledge all by name (ten semesters worth of students have used it!), but the following have made especially large contributions: Aynur Abdurazik, Muhammad Abdulla, Yuquin Ding, Jyothi Chinman, Blaine Donley, Patrick Emery, Brian Geary, Mark Hinkle, Justin Hollingsworth, John King, Yuelan Li, Xiaojuan Liu, Chris Magrin, Jyothi Reddy, Raimi Rufai, Jeremy Schneider, Bill Shelton, Frank Shukis, Quansheng Xiao, and Linzhen Xue. We especially appreciate those who generously provided extensive comments on the entire book: Guillermo Calderon-Meza, Becky Hartley, Gary Kaminski, and Andrew J. Offutt. We gratefully acknowledge the feedback of early adopters at other educational institutions: Roger Alexander, Jane Hayes, Ling Liu, Darko Marinov, Arthur Reyes, Michael Shin, and Tao Xie. We also want to acknowledge several people who provided material for the book: Roger Alexander, Mats Grindal, Hong Huang, Gary Kaminski, Robert Nilsson, Greg Williams, Wuzhi Xu. We were lucky to receive ex´ Bryce, Kim King, Sharon Ritchey, cellent suggestion from Lionel Briand, Renee Bo Sanden, and Steve Schach. We are grateful to our editor, Heather Bergman,
for providing unwavering support and enforcing the occasional deadline to move the project along, as well as Kerry Cahill from Cambridge University Press for very strong support on this project. We also acknowledge George Mason University for supporting both of us on sabbaticals and for providing GTA support at crucial times. Our department Chair, Hassan Gomaa, has enthusiastically supported this effort. Finally, of course none of this is possible without the support of our families. Thanks to Becky, Jian, Steffi, Matt, Joyce, and Andrew for keeping us grounded in reality and helping keep us happy for the past five years. Just as all programs contain faults, all texts contain errors. Our text is no different. And, as responsibility for software faults rests with the developers, responsibility for errors in this text rests with us, the authors. In particular, the bibliographic notes sections reflect our perspective of the testing field, a body of work we readily acknowledge as large and complex. We apologize in advance for omissions, and invite pointers to relevant citations. Paul Ammann Jeff Offutt
The ideas and techniques of software testing have become essential knowledge for all software developers. A software developer can expect to use the concepts presented in this book many times during his or her career. This chapter introduces the subject of software testing by describing the activities of a test engineer, defining a number of key terms, and then explaining the central notion of test coverage. Software is a key ingredient in many of the devices and systems that pervade our society. Software defines the behavior of network routers, financial networks, telephone switching networks, the Web, and other infrastructure of modern life. Software is an essential component of embedded applications that control exotic applications such as airplanes, spaceships, and air traffic control systems, as well as mundane appliances such as watches, ovens, cars, DVD players, garage door openers, cell phones, and remote controllers. Modern households have over 50 processors, and some new cars have over 100; all of them running software that optimistic consumers assume will never fail! Although many factors affect the engineering of reliable software, including, of course, careful design and sound process management, testing is the primary method that the industry uses to evaluate software under development. Fortunately, a few basic software testing concepts can be used to design tests for a large variety of software applications. A goal of this book is to present these concepts in such a way that the student or practicing engineer can easily apply them to any software testing situation. This textbook differs from other software testing books in several respects. The most important difference is in how it views testing techniques. In his landmark book Software Testing Techniques, Beizer wrote that testing is simple – all a tester needs to do is "find a graph and cover it." Thanks to Beizer's insight, it became evident to us that the myriad testing techniques present in the literature have much more in common than is obvious at first glance. Testing techniques typically are presented in the context of a particular software artifact (for example, a requirements document or code) or a particular phase of the lifecycle (for example, requirements analysis or implementation). Unfortunately, such a presentation obscures the underlying similarities between techniques. This book clarifies these similarities. 3
It turns out that graphs do not characterize all testing techniques well; other abstract models are necessary. Much to our surprise, we have found that a small number of abstract models suffice: graphs, logical expressions, input domain characterizations, and syntactic descriptions. The main contribution of this book is to simplify testing by classifying coverage criteria into these four categories, and this is why Part II of this book has exactly four chapters. This book provides a balance of theory and practical application, thereby presenting testing as a collection of objective, quantitative activities that can be measured and repeated. The theory is based on the published literature, and presented without excessive formalism. Most importantly, the theoretical concepts are presented when needed to support the practical activities that test engineers follow. That is, this book is intended for software developers.
1.1 ACTIVITIES OF A TEST ENGINEER In this book, a test engineer is an information technology (IT) professional who is in charge of one or more technical test activities, including designing test inputs, producing test case values, running test scripts, analyzing results, and reporting results to developers and managers. Although we cast the description in terms of test engineers, every engineer involved in software development should realize that he or she sometimes wears the hat of a test engineer. The reason is that each software artifact produced over the course of a product's development has, or should have, an associated set of test cases, and the person best positioned to define these test cases is often the designer of the artifact. A test manager is in charge of one or more test engineers. Test managers set test policies and processes, interact with other managers on the project, and otherwise help the engineers do their work. Figure 1.1 shows some of the major activities of test engineers. A test engineer must design tests by creating test requirements. These requirements are then
Test Engineer
Test Designs
Executable Tests
Figure 1.1. Activities of test engineers.
transformed into actual values and scripts that are ready for execution. These executable tests are run against the software, denoted P in the figure, and the results are evaluated to determine if the tests reveal a fault in the software. These activities may be carried out by one person or by several, and the process is monitored by a test manager. One of a test engineer's most powerful tools is a formal coverage criterion. Formal coverage criteria give test engineers ways to decide what test inputs to use during testing, making it more likely that the tester will find problems in the program and providing greater assurance that the software is of high quality and reliability. Coverage criteria also provide stopping rules for the test engineers. The technical core of this book presents the coverage criteria that are available, describes how they are supported by tools (commercial and otherwise), explains how they can best be applied, and suggests how they can be integrated into the overall development process. Software testing activities have long been categorized into levels, and two kinds of levels have traditionally been used. The most often used level categorization is based on traditional software process steps. Although most types of tests can only be run after some part of the software is implemented, tests can be designed and constructed during all software development steps. The most time-consuming parts of testing are actually the test design and construction, so test activities can and should be carried out throughout development. The second-level categorization is based on the attitude and thinking of the testers.
1.1.1 Testing Levels Based on Software Activity Tests can be derived from requirements and specifications, design artifacts, or the source code. A different level of testing accompanies each distinct software development activity:
Acceptance Testing – assess software with respect to requirements. System Testing – assess software with respect to architectural design. Integration Testing – assess software with respect to subsystem design. Module Testing – assess software with respect to detailed design. Unit Testing – assess software with respect to implementation.
Figure 1.2 illustrates a typical scenario for testing levels and how they relate to software development activities by isolating each step. Information for each test level is typically derived from the associated development activity. Indeed, standard advice is to design the tests concurrently with each development activity, even though the software will not be in an executable form until the implementation phase. The reason for this advice is that the mere process of explicitly articulating tests can identify defects in design decisions that otherwise appear reasonable. Early identification of defects is by far the best means of reducing their ultimate cost. Note that this diagram is not intended to imply a waterfall process. The synthesis and analysis activities generically apply to any development process. The requirements analysis phase of software development captures the customer's needs. Acceptance testing is designed to determine whether the completed software in fact meets these needs. In other words, acceptance testing probes
Acceptance Test Test Design
System Test
Subsystem Design
Detailed Design
Integration Test
Module Test
Figure 1.2. Software development activities and testing levels – the "V Model".
whether the software does what the users want. Acceptance testing must involve users or other individuals who have strong domain knowledge. The architectural design phase of software development chooses components and connectors that together realize a system whose specification is intended to meet the previously identified requirements. System testing is designed to determine whether the assembled system meets its specifications. It assumes that the pieces work individually, and asks if the system works as a whole. This level of testing usually looks for design and specification problems. It is a very expensive place to find lower-level faults and is usually not done by the programmers, but by a separate testing team. The subsystem design phase of software development specifies the structure and behavior of subsystems, each of which is intended to satisfy some function in the overall architecture. Often, the subsystems are adaptations of previously developed software. Integration testing is designed to assess whether the interfaces between modules (defined below) in a given subsystem have consistent assumptions and communicate correctly. Integration testing must assume that modules work correctly. Some testing literature uses the terms integration testing and system testing interchangeably; in this book, integration testing does not refer to testing the integrated system or subsystem. Integration testing is usually the responsibility of members of the development team. The detailed design phase of software development determines the structure and behavior of individual modules. A program unit, or procedure, is one or more contiguous program statements, with a name that other parts of the software use to call it. Units are called functions in C and C++, procedures or functions in Ada, methods in Java, and subroutines in Fortran. A module is a collection of related units that are assembled in a file, package, or class. This corresponds to a file in C, a package in Ada, and a class in C++ and Java. Module testing is designed to assess individual modules in isolation, including how the component units interact with each other and their associated data structures. Most software development organizations make module testing the responsibility of the programmer.
Implementation is the phase of software development that actually produces code. Unit testing is designed to assess the units produced by the implementation phase and is the "lowest" level of testing. In some cases, such as when building general-purpose library modules, unit testing is done without knowledge of the encapsulating software application. As with module testing, most software development organizations make unit testing the responsibility of the programmer. It is straightforward to package unit tests together with the corresponding code through the use of tools such as JUnit for Java classes. Not shown in Figure 1.2 is regression testing, a standard part of the maintenance phase of software development. Regression testing is testing that is done after changes are made to the software, and its purpose is to help ensure that the updated software still possesses the functionality it had before the updates. Mistakes in requirements and high-level design wind up being implemented as faults in the program; thus testing can reveal them. Unfortunately, the software faults that come from requirements and design mistakes are visible only through testing months or years after the original mistake. The effects of the mistake tend to be dispersed throughout multiple software components; hence such faults are usually difficult to pin down and expensive to correct. On the positive side, even if tests cannot be executed, the very process of defining tests can identify a significant fraction of the mistakes in requirements and design. Hence, it is important for test planning to proceed concurrently with requirements analysis and design and not be put off until late in a project. Fortunately, through techniques such as use-case analysis, test planning is becoming better integrated with requirements analysis in standard software practice. Although most of the literature emphasizes these levels in terms of when they are applied, a more important distinction is on the types of faults that we are looking for. The faults are based on the software artifact that we are testing, and the software artifact that we derive the tests from. For example, unit and module tests are derived to test units and modules, and we usually try to find faults that can be found when executing the units and modules individually. One of the best examples of the differences between unit testing and system testing can be illustrated in the context of the infamous Pentium bug. In 1994, Intel introduced its Pentium microprocessor, and a few months later, Thomas Nicely, a mathematician at Lynchburg College in Virginia, found that the chip gave incorrect answers to certain floating-point division calculations. The chip was slightly inaccurate for a few pairs of numbers; Intel claimed (probably correctly) that only one in nine billion division operations would exhibit reduced precision. The fault was the omission of five entries in a table of 1,066 values (part of the chip's circuitry) used by a division algorithm. The five entries should have contained the constant +2, but the entries were not initialized and contained zero instead. The MIT mathematician Edelman claimed that "the bug in the Pentium was an easy mistake to make, and a difficult one to catch," an analysis that misses one of the essential points. This was a very difficult mistake to find during system testing, and indeed, Intel claimed to have run millions of tests using this table. But the table entries were left empty because a loop termination condition was incorrect; that is, the loop stopped storing numbers before it was finished. This turns out to be a very simple fault to find during unit testing; indeed analysis showed that almost any unit level coverage criterion would have found this multimillion dollar mistake.
The Pentium bug not only illustrates the difference in testing levels, but it is also one of the best arguments for paying more attention to unit testing. There are no shortcuts – all aspects of software need to be tested. On the other hand, some faults can only be found at the system level. One dramatic example was the launch failure of the first Ariane 5 rocket, which exploded 37 seconds after liftoff on June 4, 1996. The low-level cause was an unhandled floating-point conversion exception in an internal guidance system function. It turned out that the guidance system could never encounter the unhandled exception when used on the Ariane 4 rocket. In other words, the guidance system function is correct for Ariane 4. The developers of the Ariane 5 quite reasonably wanted to reuse the successful inertial guidance system from the Ariane 4, but no one reanalyzed the software in light of the substantially different flight trajectory of Ariane 5. Furthermore, the system tests that would have found the problem were technically difficult to execute, and so were not performed. The result was spectacular – and expensive! Another public failure was the Mars lander of September 1999, which crashed due to a misunderstanding in the units of measure used by two modules created by separate software groups. One module computed thruster data in English units and forwarded the data to a module that expected data in metric units. This is a very typical integration fault (but in this case enormously expensive, both in terms of money and prestige). One final note is that object-oriented (OO) software changes the testing levels. OO software blurs the distinction between units and modules, so the OO software testing literature has developed a slight variation of these levels. Intramethod testing is when tests are constructed for individual methods. Intermethod testing is when pairs of methods within the same class are tested in concert. Intraclass testing is when tests are constructed for a single entire class, usually as sequences of calls to methods within the class. Finally, interclass testing is when more than one class is tested at the same time. The first three are variations of unit and module testing, whereas interclass testing is a type of integration testing.
1.1.2 Beizer's Testing Levels Based on Test Process Maturity Another categorization of levels is based on the test process maturity level of an organization. Each level is characterized by the goal of the test engineers. The following material is adapted from Beizer [29]. Level 0 There's no difference between testing and debugging. Level 1 The purpose of testing is to show that the software works. Level 2 The purpose of testing is to show that the software doesn't work. Level 3 The purpose of testing is not to prove anything specific, but to reduce the risk of using the software. Level 4 Testing is a mental discipline that helps all IT professionals develop higher quality software. Level 0 is the view that testing is the same as debugging. This is the view that is naturally adopted by many undergraduate computer science majors. In most CS programming classes, the students get their programs to compile, then debug the programs with a few inputs chosen either arbitrarily or provided by the professor.
This model does not distinguish between a program's incorrect behavior and a mistake within the program, and does very little to help develop software that is reliable or safe. In Level 1 testing, the purpose is to show correctness. While a significant step up from the naive level 0, this has the unfortunate problem that in any but the most trivial of programs, correctness is virtually impossible to either achieve or demonstrate. Suppose we run a collection of tests and find no failures. What do we know? Should we assume that we have good software or just bad tests? Since the goal of correctness is impossible, test engineers usually have no strict goal, real stopping rule, or formal test technique. If a development manager asks how much testing remains to be done, the test manager has no way to answer the question. In fact, test managers are in a powerless position because they have no way to quantitatively express or evaluate their work. In Level 2 testing, the purpose is to show failures. Although looking for failures is certainly a valid goal, it is also a negative goal. Testers may enjoy finding the problem, but the developers never want to find problems – they want the software to work (level 1 thinking is natural for the developers). Thus, level 2 testing puts testers and developers into an adversarial relationship, which can be bad for team morale. Beyond that, when our primary goal is to look for failures, we are still left wondering what to do if no failures are found. Is our work done? Is our software very good, or is the testing weak? Having confidence in when testing is complete is an important goal for all testers. The thinking that leads to Level 3 testing starts with the realization that testing can show the presence, but not the absence, of failures. This lets us accept the fact that whenever we use software, we incur some risk. The risk may be small and the consequences unimportant, or the risk may be great and the consequences catastrophic, but risk is always there. This allows us to realize that the entire development team wants the same thing – to reduce the risk of using the software. In level 3 testing, both testers and developers work together to reduce risk. Once the testers and developers are on the same "team," an organization can progress to real Level 4 testing. Level 4 thinking defines testing as a mental discipline that increases quality. Various ways exist to increase quality, of which creating tests that cause the software to fail is only one. Adopting this mindset, test engineers can become the technical leaders of the project (as is common in many other engineering disciplines). They have the primary responsibility of measuring and improving software quality, and their expertise should help the developers. An analogy that Beizer used is that of a spell checker. We often think that the purpose of a spell checker is to find misspelled words, but in fact, the best purpose of a spell checker is to improve our ability to spell. Every time the spell checker finds an incorrectly spelled word, we have the opportunity to learn how to spell the word correctly. The spell checker is the "expert" on spelling quality. In the same way, level 4 testing means that the purpose of testing is to improve the ability of the developers to produce high quality software. The testers should train your developers. As a reader of this book, you probably start at level 0, 1, or 2. Most software developers go through these levels at some stage in their careers. If you work in software development, you might pause to reflect on which testing level describes your company or team. The rest of this chapter should help you move to level 2 thinking, and to understand the importance of level 3. Subsequent chapters will give
you the knowledge, skills, and tools to be able to work at level 3. The ultimate goal of this book is to provide a philosophical basis that will allow readers to become "change agents" in their organizations for level 4 thinking, and test engineers to become software quality experts.
1.1.3 Automation of Test Activities Software testing is expensive and labor intensive. Software testing requires up to 50% of software development costs, and even more for safety-critical applications. One of the goals of software testing is to automate as much as possible, thereby significantly reducing its cost, minimizing human error, and making regression testing easier. Software engineers sometimes distinguish revenue tasks, which contribute directly to the solution of a problem, from excise tasks, which do not. For example, compiling a Java class is a classic excise task because, although necessary for the class to become executable, compilation contributes nothing to the particular behavior of that class. In contrast, determining which methods are appropriate to define a given data abstraction as a Java class is a revenue task. Excise tasks are candidates for automation; revenue tasks are not. Software testing probably has more excise tasks than any other aspect of software development. Maintaining test scripts, rerunning tests, and comparing expected results with actual results are all common excise tasks that routinely consume large chunks of test engineer's time. Automating excise tasks serves the test engineer in many ways. First, eliminating excise tasks eliminates drudgery, thereby making the test engineers job more satisfying. Second, automation frees up time to focus on the fun and challenging parts of testing, namely the revenue tasks. Third, automation can help eliminate errors of omission, such as failing to update all the relevant files with the new set of expected results. Fourth, automation eliminates some of the variance in test quality caused by differences in individual's abilities. Many testing tasks that defied automation in the past are now candidates for such treatment due to advances in technology. For example, generating test cases that satisfy given test requirements is typically a hard problem that requires intervention from the test engineer. However, there are tools, both research and commercial, that automate this task to varying degrees.
EXERCISES Section 1.1. 1. What are some of the factors that would help a development organization move from Beizer's testing level 2 (testing is to show errors) to testing level 4 (a mental discipline that increases quality)? 2. The following exercise is intended to encourage you to think of testing in a more rigorous way than you may be used to. The exercise also hints at the strong relationship between specification clarity, faults, and test cases.1 (a) Write a Java method with the signature public static Vector union (Vector a, Vector b) The method should return a Vector of objects that are in either of the two argument Vectors.
(b) Upon reflection, you may discover a variety of defects and ambiguities in the given assignment. In other words, ample opportunities for faults exist. Identify as many possible faults as you can. (Note: Vector is a Java Collection class. If you are using another language, interpret Vector as a list.) (c) Create a set of test cases that you think would have a reasonable chance of revealing the faults you identified above. Document a rationale for each test in your test set. If possible, characterize all of your rationales in some concise summary. Run your tests against your implementation. (d) Rewrite the method signature to be precise enough to clarify the defects and ambiguities identified earlier. You might wish to illustrate your specification with examples drawn from your test cases.
1.2 SOFTWARE TESTING LIMITATIONS AND TERMINOLOGY As said in the previous section, one of the most important limitations of software testing is that testing can show only the presence of failures, not their absence. This is a fundamental, theoretical limitation; generally speaking, the problem of finding all failures in a program is undecidable. Testers often call a successful (or effective) test one that finds an error. While this is an example of level 2 thinking, it is also a characterization that is often useful and that we will use later in this book. The rest of this section presents a number of terms that are important in software testing and that will be used later in this book. Most of these are taken from standards documents, and although the phrasing is ours, we try to be consistent with the standards. Useful standards for reading in more detail are the IEEE Standard Glossary of Software Engineering Terminology, DOD-STD-2167A and MIL-STD-498 from the US Department of Defense, and the British Computer Society's Standard for Software Component Testing. One of the most important distinctions to make is between validation and verification. Definition 1.1 Validation: The process of evaluating software at the end of software development to ensure compliance with intended usage. Definition 1.2 Verification: The process of determining whether the products of a given phase of the software development process fulfill the requirements established during the previous phase. Verification is usually a more technical activity that uses knowledge about the individual software artifacts, requirements, and specifications. Validation usually depends on domain knowledge; that is, knowledge of the application for which the software is written. For example, validation of software for an airplane requires knowledge from aerospace engineers and pilots. The acronym "IV&V" stands for "independent verification and validation," where "independent" means that the evaluation is done by nondevelopers. Sometimes the IV&V team is within the same project, sometimes the same company, and sometimes it is entirely an external entity. In part because of the independent nature of IV&V, the process often is not started until the software is complete and is often done by people whose expertise is in the application domain rather than software
development. This can sometimes mean that validation is given more weight than verification. Two terms that we have already used are fault and failure. Understanding this distinction is the first step in moving from level 0 thinking to level 1 thinking. We adopt the definition of software fault, error, and failure from the dependability community. Definition 1.3 Software Fault: A static defect in the software. Definition 1.4 Software Error: An incorrect internal state that is the manifestation of some fault. Definition 1.5 Software Failure: External, incorrect behavior with respect to the requirements or other description of the expected behavior. Consider a medical doctor making a diagnosis for a patient. The patient enters the doctor's office with a list of failures (that is, symptoms). The doctor then must discover the fault, or root cause of the symptom. To aid in the diagnosis, a doctor may order tests that look for anomalous internal conditions, such as high blood pressure, an irregular heartbeat, high levels of blood glucose, or high cholesterol. In our terminology, these anomalous internal conditions correspond to errors. While this analogy may help the student clarify his or her thinking about faults, errors, and failures, software testing and a doctor's diagnosis differ in one crucial way. Specifically, faults in software are design mistakes. They do not appear spontaneously, but rather exist as a result of some (unfortunate) decision by a human. Medical problems (as well as faults in computer system hardware), on the other hand, are often a result of physical degradation. This distinction is important because it explains the limits on the extent to which any process can hope to control software faults. Specifically, since no foolproof way exists to catch arbitrary mistakes made by humans, we cannot eliminate all faults from software. In colloquial terms, we can make software development foolproof, but we cannot, and should not attempt to, make it damn-foolproof. For a more technical example of the definitions of fault, error, and failure, consider the following Java method: public static int numZero (int[] x) { // Effects: if x == null throw NullPointerException // else return the number of occurrences of 0 in x int count = 0; for (int i = 1; i < x.length; i++) { if (x[i] == 0) { count++; } } return count; }
The fault in this program is that it starts looking for zeroes at index 1 instead of index 0, as is necessary for arrays in Java. For example, numZero ([2, 7, 0]) correctly evaluates to 1, while numZero ([0, 7, 2]) incorrectly evaluates to 0. In both of these cases the fault is executed. Although both of these cases result in an error, only the second case results in failure. To understand the error states, we need to identify the state for the program. The state for numZero consists of values for the variables x, count, i, and the program counter (denoted PC). For the first example given above, the state at the if statement on the very first iteration of the loop is ( x = [2, 7, 0], count = 0, i = 1, PC = if). Notice that this state is in error precisely because the value of i should be zero on the first iteration. However, since the value of count is coincidentally correct, the error state does not propagate to the output, and hence the software does not fail. In other words, a state is in error simply if it is not the expected state, even if all of the values in the state, considered in isolation, are acceptable. More generally, if the required sequence of states is s0 , s1 , s2 , . . . , and the actual sequence of states is s0 , s2 , s3 , . . . , then state s2 is in error in the second sequence. In the second case the corresponding (error) state is (x = [0, 7, 2], count = 0, i = 1, PC = if). In this case, the error propagates to the variable count and is present in the return value of the method. Hence a failure results. The definitions of fault and failure allow us to distinguish testing from debugging. Definition 1.6 Testing: Evaluating software by observing its execution. Definition 1.7 Test Failure: Execution that results in a failure. Definition 1.8 Debugging: The process of finding a fault given a failure. Of course the central issue is that for a given fault, not all inputs will "trigger" the fault into creating incorrect output (a failure). Also, it is often very difficult to relate a failure to the associated fault. Analyzing these ideas leads to the fault/failure model, which states that three conditions must be present for a failure to be observed. 1. The location or locations in the program that contain the fault must be reached (Reachability). 2. After executing the location, the state of the program must be incorrect (Infection). 3. The infected state must propagate to cause some output of the program to be incorrect (Propagation). This "RIP" model is very important for coverage criteria such as mutation (Chapter 5) and for automatic test data generation. It is important to note that the RIP model applies even in the case of faults of omission. In particular, when execution traverses the missing code, the program counter, which is part of the internal state, necessarily has the wrong value. The next definitions are less standardized and the literature varies widely. The definitions are our own but are consistent with common usage. A test engineer must recognize that tests include more than just input values, but are actually multipart
software artifacts. The piece of a test case that is referred to the most often is what we call the test case value. Definition 1.9 Test Case Values: The input values necessary to complete some execution of the software under test. Note that the definition of test case values is quite broad. In a traditional batch environment, the definition is extremely clear. In a Web application, a complete execution might be as small as the generation of part of a simple Web page, or it might be as complex as the completion of a set of commercial transactions. In a real-time system such as an avionics application, a complete execution might be a single frame, or it might be an entire flight. Test case values are the inputs to the program that test engineers typically focus on during testing. They really define what sort of testing we will achieve. However, test case values are not enough. In addition to test case values, other inputs are often needed to run a test. These inputs may depend on the source of the tests, and may be commands, user inputs, or a software method to call with values for its parameters. In order to evaluate the results of a test, we must know what output a correct version of the program would produce for that test. Definition 1.10 Expected Results: The result that will be produced when executing the test if and only if the program satisfies its intended behavior. Two common practical problems associated with software testing are how to provide the right values to the software and observing details of the software's behavior. These two ideas are used to refine the definition of a test case. Definition 1.11 Software Observability: How easy it is to observe the behavior of a program in terms of its outputs, effects on the environment, and other hardware and software components. Definition 1.12 Software Controllability: How easy it is to provide a program with the needed inputs, in terms of values, operations, and behaviors. These ideas are easily illustrated in the context of embedded software. Embedded software often does not produce output for human consumption, but affects the behavior of some piece of hardware. Thus, observability will be quite low. Likewise, software for which all inputs are values entered from a keyboard is easy to control. But an embedded program that gets its inputs from hardware sensors is more difficult to control and some inputs may be difficult, dangerous or impossible to supply (for example, how does the automatic pilot behave when a train jumps off-track). Many observability and controllability problems can be addressed with simulation, by extra software built to "bypass" the hardware or software components that interfere with testing. Other applications that sometimes have low observability and controllability include component-based software, distributed software and Web applications. Depending on the software, the level of testing, and the source of the tests, the tester may need to supply other inputs to the software to affect controllability or observability. For example, if we are testing software for a mobile telephone, the test case values may be long distance phone numbers. We may also need to turn the
phone on to put it in the appropriate state and then we may need to press "talk" and "end" buttons to view the results of the test case values and terminate the test. These ideas are formalized as follows. Definition 1.13 Prefix Values: Any inputs necessary to put the software into the appropriate state to receive the test case values. Definition 1.14 Postfix Values: Any inputs that need to be sent to the software after the test case values are sent. Postfix values can be subdivided into two types. Definition 1.15 Verification Values: Values necessary to see the results of the test case values. Definition 1.16 Exit Commands: Values needed to terminate the program or otherwise return it to a stable state. A test case is the combination of all these components (test case values, expected results, prefix values, and postfix values). When it is clear from context, however, we will follow tradition and use the term "test case" in place of "test case values." Definition 1.17 Test Case: A test case is composed of the test case values, expected results, prefix values, and postfix values necessary for a complete execution and evaluation of the software under test. We provide an explicit definition for a test set to emphasize that coverage is a property of a set of test cases, rather than a property of a single test case. Definition 1.18 Test Set: A test set is simply a set of test cases. Finally, wise test engineers automate as many test activities as possible. A crucial way to automate testing is to prepare the test inputs as executable tests for the software. This may be done as Unix shell scripts, input files, or through the use of a tool that can control the software or software component being tested. Ideally, the execution should be complete in the sense of running the software with the test case values, getting the results, comparing the results with the expected results, and preparing a clear report for the test engineer. Definition 1.19 Executable Test Script: A test case that is prepared in a form to be executed automatically on the test software and produce a report. The only time a test engineer would not want to automate is if the cost of automation outweighs the benefits. For example, this may happen if we are sure the test will only be used once or if the automation requires knowledge or skills that the test engineer does not have.
EXERCISES Section 1.2. 1. For what do testers use automation? What are the limitations of automation? 2. How are faults and failures related to testing and debugging?
3. Below are four faulty programs. Each includes a test case that results in failure. Answer the following questions about each program. public int findLast (int[] x, int y) { //Effects: If x==null throw NullPointerException // else return the index of the last element // in x that equals y. // If no such element exists, return -1 for (int i=x.length-1; i > 0; i--) { if (x[i] == y) { return i; } } return -1; } // test: x=[2, 3, 5]; y = 2 // Expected = 0 public int countPositive (int[] x) { //Effects: If x==null throw NullPointerException // else return the number of // positive elements in x. int count = 0; for (int i=0; i < x.length; i++) { if (x[i] >= 0) { count++; } } return count; } // test: x=[-4, 2, 0, 2] // Expected = 2
public static int lastZero (int[] x) { //Effects: if x==null throw NullPointerException // else return the index of the LAST 0 in x. // Return -1 if 0 does not occur in x for (int i = 0; i < x.length; i++) { if (x[i] == 0) { return i; } } return -1; } // test: x=[0, 1, 0] // Expected = 2 public static int oddOrPos(int[] x) { //Effects: if x==null throw NullPointerException // else return the number of elements in x that // are either odd or positive (or both) int count = 0; for (int i = 0; i < x.length; i++) { if (x[i]% 2 == 1 || x[i] > 0) { count++; } } return count; } // test: x=[-3, -2, 0, 1, 4] // Expected = 3
(a) Identify the fault. (b) If possible, identify a test case that does not execute the fault. (c) If possible, identify a test case that executes the fault, but does not result in an error state. (d) If possible identify a test case that results in an error, but not a failure. Hint: Don't forget about the program counter. (e) For the given test case, identify the first error state. Be sure to describe the complete state. (f) Fix the fault and verify that the given test now produces the expected output.
1.3 COVERAGE CRITERIA FOR TESTING Some ill-defined terms occasionally used in testing are "complete testing," "exhaustive testing," and "full coverage." These terms are poorly defined because of a fundamental theoretical limitation of software. Specifically, the number of potential inputs for most programs is so large as to be effectively infinite. Consider a Java compiler – the number of potential inputs to the compiler is not just all Java programs, or even all almost correct Java programs, but all strings. The only limitation is the size of the file that can be read by the parser. Therefore, the number of inputs is effectively infinite and cannot be explicitly enumerated.
This is where formal coverage criteria come in. Since we cannot test with all inputs, coverage criteria are used to decide which test inputs to use. The software testing community believes that effective use of coverage criteria makes it more likely that test engineers will find faults in a program and provides informal assurance that the software is of high quality and reliability. While this is, perhaps, more an article of faith than a scientifically supported proposition, it is, in our view, the best option currently available. From a practical perspective, coverage criteria provide useful rules for when to stop testing. This book defines coverage criteria in terms of test requirements. The basic idea is that we want our set of test cases to have various properties, each of which is provided (or not) by an individual test case.2 Definition 1.20 Test Requirement: A test requirement is a specific element of a software artifact that a test case must satisfy or cover. Test requirements usually come in sets, and we use the abbreviation TR to denote a set of test requirements. Test requirements can be described with respect to a variety of software artifacts, including the source code, design components, specification modeling elements, or even descriptions of the input space. Later in this book, test requirements will be generated from all of these. Let's begin with a non-software example. Suppose we are given the enviable task of testing bags of jelly beans. We need to come up with ways to sample from the bags. Suppose these jelly beans have the following six flavors and come in four colors: Lemon (colored Yellow), Pistachio (Green), Cantaloupe (Orange), Pear (White), Tangerine (also Orange), and Apricot (also Yellow). A simple approach to testing might be to test one jelly bean of each flavor. Then we have six test requirements, one for each flavor. We satisfy the test requirement "Lemon" by selecting and, of course, tasting a Lemon jelly bean from a bag of jelly beans. The reader might wish to ponder how to decide, prior to the tasting step, if a given Yellow jelly bean is Lemon or Apricot. This dilemma illustrates a classic controllability issue. As a more software-oriented example, if the goal is to cover all decisions in the program (branch coverage), then each decision leads to two test requirements, one for the decision to evaluate to false, and one for the decision to evaluate to true. If every method must be called at least once (call coverage), each method leads to one test requirement. A coverage criterion is simply a recipe for generating test requirements in a systematic way: Definition 1.21 Coverage Criterion: A coverage criterion is a rule or collection of rules that impose test requirements on a test set. That is, the criterion describes the test requirements in a complete and unambiguous manner. The "flavor criterion" yields a simple strategy for selecting jelly beans. In this case, the set of test requirements, T R, can be formally written out as T R = {flavor = Lemon, flavor = Pistachio, flavor = Cantaloupe, flavor = Pear, flavor = Tangerine, flavor = Apricot}
Test engineers need to know how good a collection of tests is, so we measure test sets against a criterion in terms of coverage. Definition 1.22 Coverage: Given a set of test requirements T R for a coverage criterion C, a test set T satisfies C if and only if for every test requirement tr in T R, at least one test t in T exists such that t satisfies tr . To continue the example, a test set T with 12 beans: three Lemon, one Pistachio, two Cantaloupe, one Pear, one Tangerine, and four Apricot satisfies the "flavor criterion." Notice that it is perfectly acceptable to satisfy a given test requirement with more than one test. Coverage is important for two reasons. First, it is sometimes expensive to satisfy a coverage criterion, so we want to compromise by trying to achieve a certain coverage level. Definition 1.23 Coverage Level: Given a set of test requirements T R and a test set T, the coverage level is simply the ratio of the number of test requirements satisfied by T to the size of T R. Second, and more importantly, some requirements cannot be satisfied. Suppose Tangerine jelly beans are rare, some bags may not contain any, or it may simply be too difficult to find a Tangerine bean. In this case, the flavor criterion cannot be 100% satisfied, and the maximum coverage level possible is 5/6 or 83%. It often makes sense to drop unsatisfiable test requirements from the set T R – or to replace them with less stringent test requirements. Test requirements that cannot be satisfied are called infeasible. Formally, no test case values exist that meet the test requirements. Examples for specific software criteria will be shown throughout the book, but some may already be familiar. Dead code results in infeasible test requirements because the statements cannot be reached. The detection of infeasible test requirements is formally undecidable for most coverage criteria, and even though some researchers have tried to find partial solutions, they have had only limited success. Thus, 100% coverage is impossible in practice. Coverage criteria are traditionally used in one of two ways. One method is to directly generate test case values to satisfy a given criterion. This method is often assumed by the research community and is the most obvious way to use criteria. It is also very hard in some cases, particularly if we do not have enough automated tools to support test case value generation. The other method is to generate test case values externally (by hand or using a pseudo-random tool, for example) and then measure the tests against the criterion in terms of their coverage. This method is usually favored by industry practitioners, because generating tests to directly satisfy the criterion is too hard. Unfortunately, this use is sometimes misleading. If our tests do not reach 100% coverage, what does that mean? We really have no data on how much, say, 99% coverage is worse than 100% coverage, or 90%, or even 75%. Because of this use of the criteria to evaluate existing test sets, coverage criteria are sometimes called metrics. This distinction actually has a strong theoretical basis. A generator is a procedure that automatically generates values to satisfy a criterion, and a recognizer is a
procedure that decides whether a given set of test case values satisfies a criterion. Theoretically, both problems are provably undecidable in the general case for most criteria. In practice, however, it is possible to recognize whether test cases satisfy a criterion far more often than it is possible to generate tests that satisfy the criterion. The primary problem with recognition is infeasible test requirements; if no infeasible test requirements are present then the problem becomes decidable. In practical terms of commercial automated test tools, a generator corresponds to a tool that automatically creates test case values. A recognizer is a coverage analysis tool. Coverage analysis tools are quite plentiful, both as commercial products and freeware. It is important to appreciate that the set T R depends on the specific artifact under test. In the jelly bean example, the test requirement color = Purple doesn't make sense because we assumed that the factory does not make Purple jelly beans. In the software context, consider statement coverage. The test requirement "Execute statement 42" makes sense only if the program under test does indeed have a statement 42. A good way to think of this issue is that the test engineer starts with a given software artifact and then chooses a particular coverage criterion. Combining the artifact with the criterion yields the specific set T R that is relevant to the test engineer's task. Coverage criteria are often related to one another, and compared in terms of subsumption. Recall that the "flavor criterion" requires that every flavor be tried once. We could also define a "color criterion," which requires that we try one jelly bean of each color {yellow, green, orange, white}. If we satisfy the flavor criterion, then we have also implicitly satisfied the color criterion. This is the essence of subsumption; that satisfying one criterion will guarantee that another one is satisfied. Definition 1.24 Criteria Subsumption: A coverage criterion C1 subsumes C2 if and only if every test set that satisfies criterion C1 also satisfies C2 . Note that this has to be true for every test set, not just some sets. Subsumption has a strong similarity with set subset relationships, but it is not exactly the same. Generally, a criterion C1 can subsume another C2 in one of two ways. The simpler way is if the test requirements for C1 always form a superset of the requirements for C2 . For example, another jelly bean criterion may be to try all flavors whose name begins with the letter 'C'. This would result in the test requirements {Cantaloupe}, which is a subset of the requirements for the flavor criterion: {Lemon, Pistachio, Cantaloupe, Pear, Tangerine, Apricot}. Thus, the flavor criterion subsumes the "starts-with-C" criterion. The relationship between the flavor and the color criteria illustrate the other way that subsumption can be shown. Since every flavor has a specific color, and every color is represented by at least one flavor, if we satisfy the flavor criterion we will also satisfy the color criterion. Formally, a many-to-one mapping exists between the requirements for the flavor criterion and the requirements for the color criterion. Thus, the flavor criterion subsumes the color criterion. (If a one-to-one mapping exists between requirements from two criteria, then they would subsume each other.) For a more realistic software-oriented example, consider branch and statement coverage. (These should already be familiar, at least intuitively, and will be defined
formally in Chapter 2.) If a test set has covered every branch in a program (satisfied branch coverage), then the test set is guaranteed to have covered every statement as well. Thus, the branch coverage criterion subsumes the statement coverage criterion. We will return to subsumption with more rigor and more examples in subsequent chapters.
1.3.1 Infeasibility and Subsumption A subtle relationship exists between infeasibility and subsumption. Specifically, sometimes a criterion C1 will subsume another criterion C2 if and only if all test requirements are feasible. If some test requirements in C1 are infeasible, however, C1 may not subsume C2 . Infeasible test requirements are common and occur quite naturally. Suppose we partition the jelly beans into Fruits and Nuts.3 Now, consider the Interaction Criterion, where each flavor of bean is sampled in conjunction with some other flavor in the same block. Such a criterion has a useful counterpart in the software domain in cases where feature interactions are a source of concern. So, for example, we might try Lemon with Pear or Tangerine, but we would not try Lemon with itself or with Pistachio. We might think that the Interaction Criterion subsumes the Flavor criterion, since every flavor is tried in conjunction with some other flavor. Unfortunately, in our example, Pistachio is the only member of the Nuts block, and hence the test requirement to try it with some other flavor in the Nuts block is infeasible. One possible strategy to reestablish subsumption is to replace each infeasible test requirement for the Interaction Criterion with the corresponding one from the Flavor criterion. In this example, we would simply taste Pistachio nuts by themselves. In general, it is desirable to define coverage criteria so that they are robust with respect to subsumption in the face of infeasible test requirements. This is not commonly done in the testing literature, but we make an effort to do so in this book. That said, this problem is mainly theoretical and should not overly concern practical testers. Theoretically, sometimes a coverage criterion C1 will subsume another C2 if we assume that C1 has no infeasible test requirements, but if C1 does create an infeasible test requirement for a program, a test suite that satisfies C1 while skipping the infeasible test requirements might also "skip" some test requirements from C2 that are satisfiable. In practice, only a few test requirements for C1 are infeasible for any given program, and if some are, it is often true that corresponding test requirements in C2 will also be infeasible. If not, the few test cases that are lost will probably not make a difference in the test results.
1.3.2 Characteristics of a Good Coverage Criterion Given the above discussion, an interesting question is "what makes a coverage criterion good?" Certainly, no definitive answers exist to this question, a fact that may partly explain why so many coverage criteria have been designed. However, three important issues can affect the use of coverage criteria. 1. The difficulty of computing test requirements 2. The difficulty of generating tests 3. How well the tests reveal faults
Subsumption is at best a very rough way to compare criteria. Our intuition may tell us that if one criterion subsumes another, then it should reveal more faults. However, no theoretical guarantee exists and the experimental studies have usually not been convincing and are far from complete. Nevertheless, the research community has reasonably wide agreement on relationships among some criteria. The difficulty of computing test requirements will depend on the artifact being used as well as the criterion. The fact that the difficulty of generating tests can be directly related to how well the tests reveal faults should not be surprising. A software tester must strive for balance and choose criteria that have the right cost / benefit tradeoffs for the software under test.
EXERCISES Section 1.3. 1. Suppose that coverage criterion C1 subsumes coverage criterion C2 . Further suppose that test set T1 satisfies C1 and on program P test set T2 satisfies C2 , also on P. (a) Does T1 necessarily satisfy C2 ? Explain. (b) Does T2 necessarily satisfy C1 ? Explain. (c) If P contains a fault, and T2 reveals the fault, T1 does not necessarily also reveal the fault. Explain.4 2. How else could we compare test criteria besides subsumption?
1.4 OLDER SOFTWARE TESTING TERMINOLOGY The testing research community has been very active in the past two decades, and some of our fundamental views of what and how to test have changed. This section presents some of the terminology that has been in use for many years, but for various reasons has become dated. Despite the fact that they are not as relevant now as they were at one time, these terms are still used and it is important that testing students and professionals be familiar with them. From an abstract perspective, black-box and white-box testing are very similar. In this book in particular, we present testing as proceeding from abstract models of the software such as graphs, which can as easily be derived from a black-box view or a white-box view. Thus, one of the most obvious effects of the unique philosophical structure of this book is that these two terms become obsolete. Definition 1.25 Black-box testing: Deriving tests from external descriptions of the software, including specifications, requirements, and design. Definition 1.26 White-box testing: Deriving tests from the source code internals of the software, specifically including branches, individual conditions, and statements. In the early 1980s, a discussion took place over whether testing should proceed from the top down or from the bottom up. This was an echo of a previous discussion over how to develop software. This distinction has pretty much disappeared as we
first learned that top-down testing is impractical, then OO design pretty much made the distinction obsolete. The following pair of definitions assumes that software can be viewed as a tree of software procedures, where the edges represent calls and the root of the tree is the main procedure. Definition 1.27 Top-Down Testing: Test the main procedure, then go down through procedures it calls, and so on. Definition 1.28 Bottom-Up Testing: Test the leaves in the tree (procedures that make no calls), and move up to the root. Each procedure is tested only if all of its children have been tested. OO software leads to a more general problem. The relationships among classes can be formulated as general graphs with cycles, requiring test engineers to make the difficult choice of what order to test the classes in. This problem is discussed in Chapter 6. Some parts of the literature separate static and dynamic testing as follows: Definition 1.29 Static Testing: Testing without executing the program. This includes software inspections and some forms of analysis. Definition 1.30 Dynamic Testing: Testing by executing the program with real inputs. Most of the literature currently uses "testing" to refer to dynamic testing and "static testing" is called "verification activities." We follow that use in this book and it should be pointed out that this book is only concerned with dynamic or executionbased testing. One last term bears mentioning because of the lack of definition. Test Strategy has been used to mean a variety of things, including coverage criterion, test process, and technologies used. We will avoid using it.
1.5 BIBLIOGRAPHIC NOTES All books on software testing and all researchers owe major thanks to the landmark books in 1979 by Myers [249], in 1990 by Beizer [29], and in 2000 by Binder [33]. Some excellent overviews of unit testing criteria have also been published, including one by White [349] and more recently by Zhu, Hall, and May [367]. The statement that software testing requires up to 50 percent of software development costs is from Myers and Sommerville [249, 316]. The recent text from Pezze and Young [289] reports relevant processes, principles, and techniques from the testing literature, and includes many useful classroom materials. The Pezze and Young text presents coverage criteria in the traditional lifecycle-based manner, and does not organize criteria into the four abstract models discussed in this chapter. Numerous other software testing books were not intended as textbooks, or do not offer general coverage for classroom use. Beizer's Software System Testing and Quality Assurance [28] and Hetzel's The Complete Guide to Software Testing [160] cover various aspects of management and process for software testing. Several books cover specific aspects of testing [169, 227, 301]. The STEP project at Georgia
Institute of Technology resulted in a comprehensive survey of the practice of software testing by Department of Defense contractors in the 1980s [100]. The definition of unit is from Stevens, Myers and Constantine [318], and the definition of module is from Sommerville [316]. The definition of integration testing is from Beizer [29]. The clarification for OO testing levels with the terms intra-method, inter-method, and intra-class testing is from Harrold and Rothermel [152] and inter-class testing is from Gallagher, Offutt and Cincotta [132]. The information for the Pentium bug and Mars lander was taken from several sources, including by Edelman, Moler, Nuseibeh, Knutson, and Peterson [111, 189, 244, 259, 286]. The accident report [209] is the best source for understanding the details of the Ariane 5 Flight 501 Failure. The testing levels in Section 1.1.2 were first defined by Beizer [29]. The elementary result that finding all failures in a program is undecidable is due to Howden [165]. Most of the terminology in testing is from standards documents, including the IEEE Standard Glossary of Software Engineering Terminology [175], the US Department of Defense [260, 261], the US Federal Aviation Administration FAADO178B, and the British Computer Society's Standard for Software Component Testing [317]. The definitions for observability and controllability come from Freedman [129]. Similar definitions were also given in Binder's book Testing ObjectOriented Systems [33]. The fault/failure model was developed independently by Offutt and Morell in their dissertations [101, 246, 247, 262]. Morell used the terms execution, infection, and propagation [247, 246], and Offutt used reachability, sufficiency, and necessity [101, 262]. This book merges the two sets of terms by using what we consider to be the most descriptive terms. The multiple parts of the test case that we use are based on research in test case specifications [23, 319]. One of the first discussions of infeasibility from other than a purely theoretical view was by Frankl and Weyuker [128]. The problem was shown to be undecidable by Goldberg et al. [136] and DeMillo and Offutt [101]. Some partial solutions have been presented [132, 136, 177, 273]. Budd and Angluin [51] analyzed the theoretical distinctions between generators and recognizers from a testing viewpoint. They showed that both problems are formally undecidable, and discussed tradeoffs in approximating the two. Subsumption has been widely used as a way to analytically compare testing techniques. We follow Weiss [340] and Frankl and Weyuker [128] for our definition of subsumption. Frankl and Weyuker actually used the term includes. The term subsumption was defined by Clarke et al.: A criterion C1 subsumes a criterion C2 if and only if every set of execution paths P that satisfies C1 also satisfies C2 [81]. The term subsumption is currently the more widely used and the two definitions are equivalent; this book follows Weiss's suggestion to use the term subsumes to refer to Frankl and Weyuker's definition. The descriptions of excise and revenue tasks were taken from Cooper [89]. Although this book does not focus heavily on the theoretical underpinnings of software testing, students interested in research should study such topics more in depth. A number of the papers are quite old and often do not appear in current
literature, and their ideas are beginning to disappear. The authors encourage the study of the older papers. Among those are truly seminal papers in the 1970s by Goodenough and Gerhart [138] and Howden [165], and Demillo, Lipton, Sayward, and Perlis [98, 99]. These papers were followed up and refined by Weyuker and Ostrand [343], Hamlet [147], Budd and Angluin [51], Gourlay [139], Prather [293], Howden [168], and Cherniavsky and Smith [67]. Later theoretical papers were contributed by Morell [247], Zhu [366], and Wah [335, 336]. Every PhD student's adviser will certainly have his or her own favorite theoretical papers, but this list should provide a good starting point.
NOTES 1 Liskov's Program Development in Java, especially chapters 9 and 10, is a great source for students who wish to pursue this direction further. 2 While this is a good general rule, exceptions exist. For example, test requirements for some logic coverage criteria demand pairs of related test cases instead of individual test cases. 3 The reader might wonder whether we need an Other category to ensure that we have a partition. In our example, we are ok, but in general, one would need such a category to handle jelly beans such as Potato, Spinach, or Ear Wax. 4 Correctly answering this question goes a long way towards understanding the weakness of the subsumption relation.
2 Graph Coverage
This chapter introduces the major test coverage criteria in use today. It starts out in a very theoretical way, but a firm grasp of the theoretical aspects of graphs and graph coverage makes the remainder of the chapter simpler. We first emphasize a generic view of a graph without regard to the graph's source. After this model is established, the rest of the chapter turns to practical applications by demonstrating how graphs can be obtained from various software artifacts and how the generic versions of the criteria are adapted to those graphs.
2.1 OVERVIEW Directed graphs form the foundation for many coverage criteria. Given an artifact under test, the idea is to obtain a graph abstraction of that artifact. For example, the most common graph abstraction for source code maps code to a control flow graph. It is important to understand that the graph is not the same as the artifact, and that, indeed, artifacts typically have several useful, but nonetheless quite different, graph abstractions. The same abstraction that produces the graph from the artifact also maps test cases for the artifact to paths in the graph. Accordingly, a graphbased coverage criterion evaluates a test set for an artifact in terms of how the paths corresponding to the test cases "cover" the artifact's graph abstraction. We give our basic notion of a graph below and will add additional structures later in the chapter when needed. A graph G formally is
a set a set a set a set
N of nodes N0 of initial nodes, where N0 ⊆ N N f of final nodes, where N f ⊆ N E of edges, where E is a subset of N × N
For a graph to be useful for generating tests, it is necessary for N, N0 , and N f to contain at least one node each. Sometimes, it is helpful to consider only part of a graph. A subgraph of a graph is also a graph and is defined by a subset of N, along with the corresponding subsets of N0 , N f , and E. Specifically, if Nsub is a subset of
N = { n0, n1, n2, n3 } N 0 = { n0 }
N = { n0, n1, n2, n3, n4, n5, n6, n7, n8, n9} N0 = { n0, n1, n2}
E = { (n0, n1), (n0, n2), (n1, n3),(n2, n3 ) }
|E| = 12
(a) A graph with a single initial node (b) A graph with mutiple initial nodes
N = { n0, n1, n2, n3 } |E| = 4
(c) A graph with no initial node
Figure 2.1. Graph (a) has a single initial node, graph (b) multiple initial nodes, and graph (c) (rejected) with no initial nodes.
N, then for the subgraph defined by Nsub , the set of initial nodes is Nsub ∩ N0 , the set of final nodes is Nsub ∩ N f , and the set of edges is (Nsub × Nsub ) ∩ E. Note that more than one initial node can be present; that is, N0 is a set. Having multiple initial nodes is necessary for some software artifacts, for example, if a class has multiple entry points, but sometimes we will restrict the graph to having one initial node. Edges are considered to be from one node and to another and written as (ni , n j ). The edge's initial node ni is sometimes called the predecessor and n j is called the successor. We always identify final nodes, and there must be at least one final node. The reason is that every test must start in some initial node and end in some final node. The concept of a final node depends on the kind of software artifact the graph represents. Some test criteria require tests to end in a particular final node. Other test criteria are satisfied with any node for a final node, in which case the set N f is the same as the set N. The term node has various synonyms. Graph theory texts sometimes call a node a vertex, and testing texts typically identify a node with the structure it represents, often a statement or a basic block. Similarly, graph theory texts sometimes call an edge an arc, and testing texts typically identify an edge with the structure it represents, often a branch. This section discusses graph criteria in a generic way; thus we stick to general graph terms. Graphs are often drawn with bubbles and arrows. Figure 2.1 shows three example graphs. The nodes with incoming edges but no predecessor nodes are the initial nodes. The nodes with heavy borders are final nodes. Figure 2.1(a) has a single initial node and no cycles. Figure 2.1(b) has three initial nodes, as well as a cycle ([n1 , n4 , n8 , n5 , n1 ]). Figure 2.1(c) has no initial nodes, and so is not useful for generating test cases.
Graph Coverage
Path Examples
n 0, n 3, n7 n1, n4, n8, n5, n1 n2, n 6, n9
Invalid Path Examples
n0, n 7 n3, n 4 n2, n6, n8
(a) Path examples n3
Reachability Examples
reach (n0 ) = N - { n2, n6 } reach (n0 , n1, n2 ) = N reach (n4 ) = { n1, n4, n5, n7, n8, n9 } reach ([n6, n9 ]) = { n9 }
(b) Reachability examples n7
Figure 2.2. Example of paths.
A path is a sequence [n1 , n2 , . . . , n M ] of nodes, where each pair of adjacent nodes, (ni , ni+1 ), 1 ≤ i < M, is in the set E of edges. The length of a path is defined as the number of edges it contains. We sometimes consider paths and subpaths of length zero. A subpath of a path p is a subsequence of p (possibly p itself). Following the notation for edges, we say a path is from the first node in the path and to the last node in the path. It is also useful to be able to say that a path is from (or to) an edge e, which simply means that e is the first (or last) edge in the path. Figure 2.2 shows a graph along with several example paths, and several examples that are not paths. For instance, the sequence [n0 , n7 ] is not a path because the two nodes are not connected by an edge. Many test criteria require inputs that start at one node and end at another. This is only possible if those nodes are connected by a path. When we apply these criteria on specific graphs, we sometimes find that we have asked for a path that for some reason cannot be executed. For example, a path may demand that a loop be executed zero times in a situation where the program always executes the loop at least once. This kind of problem is based on the semantics of the software artifact that the graph represents. For now, we emphasize that we are looking only at the syntax of the graph. We say that a node n (or an edge e) is syntactically reachable from node ni if there exists a path from node ni to n (or edge e). A node n (or edge e) is also semantically reachable if it is possible to execute at least one of the paths with some input. We can define the function reachG(x) as the portion of a graph that is syntactically reachable from the parameter x. The parameter for reachG() can be a node, an edge, or a set of nodes or edges. Then reachG(ni ) is the subgraph of G that is syntactically reachable from node ni , reachG(N0 ) is the subgraph of G that is syntactically reachable from any initial node, reachG(e) is the subgraph of G syntactically reachable from edge e, and so on. In our use, reachG() includes the starting nodes. For example, both reachG(ni ) and reachG([ni , n j ]) always include ni , and reachG([ni , n j ]) includes edge ([ni , n j ]). Some graphs have nodes or starting edges that cannot be syntactically reached from any of the initial nodes N0 . These graphs frustrate attempts to satisfy a coverage criterion, so we typically restrict our attention to reachG(N0 ).1
Figure 2.3. A single entry single exit graph.
Consider the examples in Figure 2.2. From n0 , it is possible to reach all nodes except n2 and n6 . From the entire set of initial nodes {n0 , n1 , n2 }, it is possible to reach all nodes. If we start at n4 , it is possible to reach all nodes except n0 , n2 , n3 , and n6 . If we start at edge (n6 , n9 ), it is possible to reach only n6 , n9 and edge (n6 , n9 ). In addition, some graphs (such as finite state machines) have explicit edges from a node to itself, that is, (ni , ni ). Basic graph algorithms, usually given in standard data structures texts, can be used to compute syntactic reachability. A test path represents the execution of a test case. The reason test paths must start in N0 is that test cases always begin from an initial node. It is important to note that a single test path may correspond to a very large number of test cases on the software. It is also possible that a test path may correspond to zero test cases if the test path is infeasible. We return to the crucial but theoretical issue of infeasibility later, in Section 2.2.1. Definition 2.31 Test path: A path p, possibly of length zero, that starts at some node in N0 and ends at some node in N f . For some graphs, all test paths start at one node and end at a single node. We call these single entry/single exit or SESE graphs. For SESE graphs, the set N0 has exactly one node, called n0 , and the set N f also has exactly one node, called n f , which may be the same as n0 . We require that n f be syntactically reachable from every node in N, and that no node in N (except n f ) be syntactically reachable from n f (unless n0 and n f are the same node). In other words, no edges start at n f , except when n0 and n f happen to be the same node. Figure 2.3 is an example of a SESE graph. This particular structure is sometimes called a "double-diamond" graph and corresponds to the control flow graph for a sequence of two if-then-else statements. The initial node, n0 , is designated with an incoming arrow (remember we only have one initial node), and the final
Graph Coverage Test Paths
Test Cases t1
In deterministic software, a many-to-one relationship exists between test cases and test paths. Test Cases t1
Test Paths Many-to-many
For nondeterministic software, a many-to-many relationship exists between test cases and test paths.
Figure 2.4. Test case mappings to test paths.
node, n6 , is designated with a thick circle. Exactly four test paths exist in the double-diamond graph: [n0 , n1 , n3 , n4 , n6 ], [n0 , n1 , n3 , n5 , n6 ], [n0 , n2 , n3 , n4 , n6 ], and [n0 , n2 , n3 , n5 , n6 ]. We need some terminology to express the notion of nodes, edges, and subpaths that appear in test paths, and choose familiar terminology from traveling. A test path p is said to visit node n if n is in p. Test path p is said to visit edge e if e is in p. The term visit applies well to single nodes and edges, but sometimes we want to turn our attention to subpaths. For subpaths, we use the term tour. Test path p is said to tour subpath q if q is a subpath of p. The first path of Figure 2.3, [n0 , n1 , n3 , n4 , n6 ], visits nodes n0 and n1 , visits edges (n0 , n1 ) and (n3 , n4 ), and tours the subpath [n1 , n3 , n4 ] (among others, these lists are not complete). Since the subpath relationship is reflexive, the tour relationship is also reflexive. That is, any given path p always tours itself. We define a mapping pathG for tests, so for a test case t, pathG(t) is the test path in graph G that is executed by t. Since it is usually obvious which graph we are discussing, we omit the subscript G. We also define the set of paths toured by a set of tests. For a test set T, path(T) is the set of test paths that are executed by the tests in T: pathG(T) = { pathG(t)|t ∈ T}. Except for nondeterministic structures, which we do not consider until Chapter 7, each test case will tour exactly one test path in graph G. Figure 2.4 illustrates the difference with respect to test case/test path mapping for deterministic vs. nondeterministic software. Figure 2.5 illustrates a set of test cases and corresponding test paths on a SESE graph with the final node n f = n2 . Some edges are annotated with predicates that describe the conditions under which that edge is traversed. (This notion is formalized later in this chapter.) So, in the example, if a is less than b, the only path is from n0 to n1 and then on to n3 and n2 . This book describes all of the graph coverage criteria in terms of relationships of test paths to the graph in question, but it is important to realize that testing is carried out with test cases, and that the test path is simply a model of the test case in the abstraction captured by the graph.
(a) Graph for testing the case with input integers a, b and output (a+b)
Map to Test case t1 : (a=0, b=1)
[ Test path p1 : n 0, n1, n3, n2 ]
Test case t2 : (a=1, b=1)
[ Test path p2 : n 0, n3, n2 ]
[ Test path p3 : n 0, n2 ]
(b) Mapping between test cases and test paths
Figure 2.5. A set of test cases and corresponding test paths.
EXERCISES Section 2.1. 1. 2. 3. 4.
Give the sets N, N0 , N f , and E for the graph in Figure 2.2. Give a path that is not a test path in Figure 2.2. List all test paths in Figure 2.2. In Figure 2.5, find test case inputs such that the corresponding test path visits edge (n1 , n3 ).
2.2 GRAPH COVERAGE CRITERIA The structure in Section 2.1 is adequate to define coverage on graphs. As is usual in the testing literature, we divide these criteria into two types. The first are usually referred to as control flow coverage criteria. Because we generalize this situation, we call them structural graph coverage criteria. The other criteria are based on the flow of data through the software artifact represented by the graph and are called data flow coverage criteria. Following the discussion in Chapter 1, we identify the appropriate test requirements and then define each criterion in terms of the test requirements. In general, for any graph-based coverage criterion, the idea is to identify the test requirements in terms of various structures in the graph. For graphs, coverage criteria define test requirements, T R, in terms of properties of test paths in a graph G. A typical test requirement is met by visiting a particular node or edge or by touring a particular path. The definitions we have given so far for a visit are adequate, but the notion of a tour requires more development. We return to the issue of touring later in this chapter and then refine it further in the context
of data flow criteria. The following definition is a refinement of the definition of coverage given in Chapter 1: Definition 2.32 Graph Coverage: Given a set T R of test requirements for a graph criterion C, a test set T satisfies C on graph G if and only if for every test requirement tr in T R, there is at least one test path p in path(T) such that p meets tr . This is a very general statement that must be refined for individual cases.
2.2.1 Structural Coverage Criteria We specify graph coverage criteria by specifying a set of test requirements, T R. We will start by defining criteria to visit every node and then every edge in a graph. The first criterion is probably familiar and is based on the old notion of executing every statement in a program. This concept has variously been called "statement coverage," "block coverage," "state coverage," and "node coverage." We use the general graph term "node coverage." Although this concept is familiar and simple, we introduce some additional notation. The notation initially seems to complicate the criterion, but ultimately has the effect of making subsequent criteria cleaner and mathematically precise, avoiding confusion with more complicated situations. The requirements that are produced by a graph criterion are technically predicates that can have either the value true (the requirement has been met) or false (the requirement has not been met). For the double-diamond graph in Figure 2.3, the test requirements for node coverage are: T R = { visit n0 , visit n1 , visit n2 , visit n3 , visit n4 , visit n5 , visit n6 }. That is, we must satisfy a predicate for each node, where the predicate asks whether the node has been visited or not. With this in mind, the formal definition of node coverage is as follows2 : Definition 2.33 Node Coverage (Formal Definition): For each node n ∈ reachG(N0 ), T R contains the predicate "visit n." This notation, although mathematically precise, is too cumbersome for practical use. Thus we choose to introduce a simpler version of the definition that abstracts the issue of predicates in the test requirements. Criterion 2.1 Node Coverage (NC): T R contains each reachable node in G. With this definition, it is left as understood that the term "contains" actually means "contains the predicate visitn ." This simplification allows us to simplify the writing of the test requirements for Figure 2.3 to only contain the nodes: T R = {n0 , n1 , n2 , n3 , n4 , n5 , n6 }. Test path p1 = [n0 , n1 , n3 , n4 , n6 ] meets the first, second, fourth, fifth, and seventh test requirements, and test path p2 = [n0 , n2 , n3 , n5 , n6 ] meets the first, third, fourth, sixth, and seventh. Therefore, if a test set T contains {t1 , t2 }, where path(t1 ) = p1 and path(t2 ) = p2 , then T satisfies node coverage on G. The usual definition of node coverage omits the intermediate step of explicitly identifying the test requirements, and is often stated as given below. Notice the economy of the form used above with respect to the standard definition. Several
n0 xy
n2 path (t1 ) = [ n0, n1, n2 ] path (t2 ) = [ n0, n2 ] T 1 = { t1 } T1 satisfies node coverage on the graph
T 2 = { t 1 , t2 } T2 satisfies edge coverage on the graph
(a) Node Coverage
(b) Edge Coverage
Figure 2.6. A graph showing node coverage and edge coverage.
of the exercises emphasize this point by directing the student to recast other criteria in the standard form. Definition 2.34 Node Coverage (NC) (Standard Definition): Test set T satisfies node coverage on graph G if and only if for every syntactically reachable node n in N, there is some path p in path(T) such that p visits n. The exercises at the end of the section have the reader reformulate the definitions of some of the remaining coverage criteria in both the formal way and the standard way. We choose the intermediate definition because it is more compact, avoids the extra verbiage in a standard coverage definition, and focuses just on the part of the definition of coverage that changes from criterion to criterion. Node coverage is implemented in many commercial testing tools, most often in the form of statement coverage. So is the next common criterion of edge coverage, usually implemented as branch coverage: Criterion 2.2 Edge Coverage (EC): T R contains each reachable path of length up to 1, inclusive, in G. The reader might wonder why the test requirements for edge coverage also explicitly include the test requirements for node coverage – that is, why the phrase "up to" is included in the definition. In fact, all the graph coverage criteria are developed like this. The motivation is subsumption for graphs that do not contain more complex structures. For example, consider a graph with a node that has no edges. Without the "up to" clause in the definition, edge coverage would not cover that node. Intuitively, we would like edge testing to be at least as demanding as node testing. This style of definition is the best way to achieve this property. To make our TR sets readable, we list only the maximal length paths. Figure 2.6 illustrates the difference between node and edge coverage. In program statement terms, this is a graph of the common "if-else" structure.
Other coverage criteria use only the graph definitions introduced so far. For example, one requirement is that each path of length (up to) two be toured by some test path. With this context, node coverage could be redefined to contain each path of length zero. Clearly, this idea can be extended to paths of any length, although possibly with diminishing returns. We formally define one of these criteria; others are left as exercises for the interested reader. Criterion 2.3 Edge-Pair Coverage (EPC): T R contains each reachable path of length up to 2, inclusive, in G. One useful testing criterion is to start the software in some state (that is, a node in the finite state machine) and then follow transitions (that is, edges) so that the last state is the same as the start state. This type of testing is used to verify that the system is not changed by certain inputs. Shortly we will formalize this notion as round trip coverage. Before defining round trip coverage, we need a few more definitions. A path from ni to n j is simple if no node appears more than once in the path, with the exception that the first and last nodes may be identical. That is, simple paths have no internal loops, although the entire path itself may wind up being a loop. One useful aspect of simple paths is that any path can be created by composing simple paths. Even fairly small programs may have a very large number of simple paths. Most of these simple paths aren't worth addressing explicitly since they are subpaths of other simple paths. For a coverage criterion for simple paths we would like to avoid enumerating the entire set of simple paths. To this end we list only maximal length simple paths. To clarify this notion, we introduce a formal definition for a maximal length simple path, which we call a prime path, and we adopt the name "prime" for the criterion: Definition 2.35 Prime Path: A path from ni to n j is a prime path if it is a simple path and it does not appear as a proper subpath of any other simple path. Criterion 2.4 Prime Path Coverage (PPC): T R contains each prime path in G. While this definition of prime path coverage has the practical advantage of keeping the number of test requirements down, it suffers from the problem that a given infeasible prime path may well incorporate many feasible simple paths. The solution is direct: replace the infeasible prime path with relevant feasible subpaths. For the purposes of this textbook, we choose not to include this aspect of prime path coverage formally in the definition, but we assume it in later theoretical characterizations of prime path coverage. Prime path coverage has two special cases that we include below for historical reasons. From a practical perspective, it is usually better simply to adopt prime path coverage. Both special cases involve treatment of loops with "round trips."
A round trip path is a prime path of nonzero length that starts and ends at the same node. One type of round trip test coverage requires at least one round trip path to be taken for each node, and another requires all possible round trip paths. Criterion 2.5 Simple Round Trip Coverage (SRTC): T R contains at least one round-trip path for each reachable node in G that begins and ends a round-trip path. Criterion 2.6 Complete Round Trip Coverage (CRTC): T R contains all roundtrip paths for each reachable node in G. Next we turn to path coverage, which is traditional in the testing literature. Criterion 2.7 Complete Path Coverage (CPC): T R contains all paths in G. Sadly, complete path coverage is useless if a graph has a cycle, since this results in an infinite number of paths, and hence an infinite number of test requirements. A variant of this criterion is, however, useful. Suppose that instead of requiring all paths, we consider a specified set of paths. For example, these paths might be given by a customer in the form of usage scenarios. Criterion 2.8 Specified Path Coverage (SPC): T R contains a set S of test paths, where S is supplied as a parameter. Complete path coverage is not feasible for graphs with cycles; hence the reason for developing the other alternatives listed above. Figure 2.7 contrasts prime path coverage with complete path coverage. Part (a) of the figure shows the "diamond" graph, which contains no loops. Both complete path coverage and prime path coverage can be satisfied on this graph with the two paths shown. Part (b), however, includes a loop from n1 to n3 to n4 to n1 , thus the graph has an infinite number of possible test paths, and complete path coverage is not possible. The requirements for prime path coverage, however, can be toured with two test paths, for example, [n0 , n1 , n2 ] and [n0 , n1 , n3 , n4 , n1 , n3 , n4 , n1 , n2 ].
Touring, Sidetrips, and Detours An important but subtle point to note is that while simple paths do not have internal loops, we do not require the test paths that tour a simple path to have this property. That is, we distinguish between the path that specifies a test requirement and the portion of the test path that meets the requirement. The advantage of separating these two notions has to do with the issue of infeasible test requirements. Before describing this advantage, let us refine the notion of a tour. We previously defined "visits" and "tours," and recall that using a path p to tour a subpath [n1 , n2 , n3 ] means that the subpath is a subpath of p. This is a rather strict definition because each node and edge in the subpath must be visited exactly in the order that they appear in the subpath. We would like to relax this a bit to allow
n3 n1
Prime Paths = { [n 0, n1, n3], [n0, n2, n3] } path (t1) = [n0, n1, n3] path (t2) = [n0, n2, n3] T1 = {t1, t2} T1 satisfies prime path coverage on the graph
Prime Paths = { [n 0, n1, n2], [n 0, n1, n3, n4], [n1, n3, n4, n1], [n 3, n4, n1, n3], [n4, n1, n3, n4], [n 3, n4, n1, n2] } path (t3) = [n0, n1, n2] path (t4) = [n0, n1, n3, n4, n1, n3, n4, n1, n2] T2 = {t3, t4} T2 satisfies prime path coverage on the graph
(a) Prime Path Coverage on a Graph with No Loops
(b) Prime Path Coverage on a Graph with Loops
Figure 2.7. Two graphs showing prime path coverage.
loops to be included in the tour. Consider the graph in Figure 2.8, which features a small loop from b to c and back. If we are required to tour subpath q = [a, b, d], the strict definition of tour prohibits us from meeting the requirement with any path that contains c, such as p = [s0 , a, b, c, b, d, s f ], because we do not visit a, b, and d in exactly the same order. We relax the tour definition in two ways. The first allows the tour to include "sidetrips," where we can leave the path temporarily from a node and then return to the same node. The second allows the tour to include more general "detours" where we can leave the path from a node and then return to the next node on the
Figure 2.8. Graph with a loop.
1 S0
3 4 c
(a) Graph being toured with a sidetrip
(b) Graph being toured with a detour Figure 2.9. Tours, sidetrips, and detours in graph coverage.
path (skipping an edge). In the following definitions, q is a required subpath that is assumed to be simple. Definition 2.36 Tour: Test path p is said to tour subpath q if and only if q is a subpath of p. Definition 2.37 Tour with Sidetrips: Test path p is said to tour subpath q with sidetrips if and only if every edge in q is also in p in the same order. Definition 2.38 Tour with Detours: Test path p is said to tour subpath q with detours if and only if every node in q is also in p in the same order. The graphs in Figure 2.9 illustrate sidetrips and detours on the graph from Figure 2.8. In Figure 2.9(a), the dashed lines show the sequence of edges that are executed in a tour with a sidetrip. The numbers on the dashed lines indicate the order in which the edges are executed. In Figure 2.9(b), the dashed lines show the sequence of edges that are executed in a tour with a detour. While these differences are rather small, they have far-reaching consequences. The difference between sidetrips and detours can be seen in Figure 2.9. The subpath [b, c, b] is a sidetrip to [a, b, d] because it leaves the subpath at node b and then returns to the subpath at node b. Thus, every edge in the subpath [a, b, d] is executed in the same order. The subpath [b, c, d] is a detour to [a, b, d] because it leaves the subpath at node b and then returns to a node in the subpath at a later point, bypassing the edge (b, d). That is, every node [a, b, d] is executed in the same order but every edge is not. Detours have the potential to drastically change the behavior of the intended test. That is, a test that takes the edge (c, d) may exhibit different
behavior and test different aspects of the program than a test that takes the edge (b, d). To use the notion of sidetrips and detours, one can "decorate" each appropriate graph coverage criterion with a choice of touring. For example, prime path coverage could be defined strictly in terms of tours, less strictly to allow sidetrips, or even less strictly to allow detours. The position taken in this book is that sidetrips are a practical way to deal with infeasible test requirements, as described below. Hence we include them explicitly in our criteria. Detours seem less practical, and so we do not include them further.
Dealing with Infeasible Test Requirements If sidetrips are not allowed, a large number of infeasible requirements can exist. Consider again the graph in Figure 2.9. In many programs it will be impossible to take the path from a to d without going through node c at least once because, for example, the loop body is written such that it cannot be skipped. If this happens, we need to allow sidetrips. That is, it may not be possible to tour the path [a, b, d] without a sidetrip. The argument above suggests dropping the strict notion of touring and simply allowing test requirements to be met with sidetrips. However, this is not always a good idea! Specifically, if a test requirement can be met without a sidetrip, then doing so is clearly superior to meeting the requirement with a sidetrip. Consider the loop example again. If the loop can be executed zero times, then the path [a, b, d] should be toured without a sidetrip. The argument above suggests a hybrid treatment with desirable practical and theoretical properties. The idea is to meet test requirements first with strict tours, and then allow sidetrips for unmet test requirements. Clearly, the argument could easily be extended to detours, but, as mentioned above, we elect not to do so. Definition 2.39 Best Effort Touring: Let T Rtour be the subset of test requirements that can be toured and T Rsidetrip be the subset of test requirements that can be toured with sidetrips. Note that T Rtour ⊆ T Rsidetrip . A set T of test paths achieves best effort touring if for every path p in T Rtour , some path in T tours p directly and for every path p in T Rsidetrip , some path in T tours p either directly or with a sidetrip. Best-effort touring has the practical benefit that as many test requirements are met as possible, yet each test requirement is met in the strictest possible way. As we will see in Section 2.2.3 on subsumption, best-effort touring has desirable theoretical properties with respect to subsumption.
Finding Prime Test Paths It turns out to be relatively simple to find all prime paths in a graph, and test paths to tour the prime paths can be constructed in a mechanical manner. Consider the example graph in Figure 2.10. It has seven nodes and nine edges, including a loop and an edge from node n4 to itself (sometimes called a "self-loop.") Prime paths can be found by starting with paths of length 0, then extending to length 1, and so on. Such an algorithm collects all simple paths, whether prime or
Figure 2.10. An example for prime test paths.
not. The prime paths can be easily screened from this set. The set of paths of length 0 is simply the set of nodes, and the set of paths of length 1 is simply the set of edges. For simplicity, we simply list the node numbers in this example. Simple paths of length 0 (7): 1) 2) 3) 4) 5) 6) 7)
[0] [1] [2] [3] [4] [5] [6] !
The exclamation point on the path [6] tells us that this path cannot be extended. Specifically, the final node 6 has no outgoing edges, and so paths that end with 6 are not extended further. Simple paths of length 1 (9): 8) 9) 10) 11) 12) 13) 14) 15) 16)
[0, 1] [0, 4] [1, 2] [1, 5] [2, 3] [3, 1] [4, 4] * [4, 6] ! [5, 6] !
The asterisk on the path [4, 4] tells us that path can go no further because the first node is the same as the last (it is already a cycle). For paths of length 2, we identify each path of length 1 that is not a cycle (marked with asterisks). We then extend the path with every node that can be reached from the final node in the path unless that node is already in the path and not the first node. The first path of length 1, [0, 1], is extended to [0, 1, 2] and [0, 1, 5]. The second, [0, 4], is extended to [0, 4, 6] but not [0, 4, 4], because node 4 is already in the path ([0, 4, 4] is not simple and thus is not prime).
Simple paths of length 2 (8): 17) 18) 19) 20) 21) 22) 23) 24)
[0, 1, 2] [0, 1, 5] [0, 4, 6] ! [1, 2, 3] [1, 5, 6] ! [2, 3, 1] [3, 1, 2] [3, 1, 5]
Paths of length 3 are computed in a similar way. Simple paths of length 3 (7): 25) 26) 27) 28) 29) 30) 31)
[0, 1, 2, 3] ! [0, 1, 5, 6] ! [1, 2, 3, 1] * [2, 3, 1, 2] * [2, 3, 1, 5] [3, 1, 2, 3] * [3, 1, 5, 6] !
Finally, only one path of length 4 exists. Three paths of length 3 cannot be extended because they are cycles; two others end with node 6. Of the remaining two, the path that ends in node 3 cannot be extended because [0, 1, 2, 3, 1] is not simple and thus is not prime. Prime paths of length 4 (1): 32) [2, 3, 1, 5, 6]! The prime paths can be computed by eliminating any path that is a (proper) subpath of some other simple path. Note that every simple path without an exclamation mark or asterisk is eliminated as it can be extended and is thus a proper subpath of some other simple path. There are eight prime paths: 14) 19) 25) 26) 27) 28) 30) 32)
[4, 4] * [0, 4, 6] ! [0, 1, 2, 3] ! [0, 1, 5, 6] ! [1, 2, 3, 1] * [2, 3, 1, 2] * [3, 1, 2, 3] * [2, 3, 1, 5, 6]!
This process is guaranteed to terminate because the length of the longest possible prime path is the number of nodes. Although graphs often have many simple paths (32 in this example, of which 8 are prime), they can usually be toured with far fewer test paths. Many possible algorithms can find test paths to tour the prime paths. Observation will suffice with a graph as simple as in Figure 2.10. For example, it can be seen that the four test paths [0, 1, 5, 6], [0, 1, 2, 3, 1, 2, 3, 1, 5, 6], [0, 4, 6],
and [0, 4, 4, 6] are enough. This approach, however, is error-prone. The easiest thing to do is to tour the loop [1, 2, 3] only once, which omits the prime paths [2, 3, 1, 2] and [3, 1, 2, 3]. With more complicated graphs, a mechanical approach is needed. We recommend starting with the longest prime paths and extending them to the beginning and end nodes in the graph. For our example, this results in the test path [0, 1, 2, 3, 1, 5, 6]. The test path [0, 1, 2, 3, 1, 5, 6] tours 3 prime paths 25, 27, and 32. The next test path is constructed by extending one of the longest remaining prime paths; we will continue to work backward and choose 30. The resulting test path is [0, 1, 2, 3, 1, 2, 3, 1, 5, 6], which tours 2 prime paths, 28 and 30 (it also tours paths 25 and 27). The next test path is constructed by using the prime path 26 [0, 1, 5, 6]. This test path tours only maximal prime path 26. Continuing in this fashion yields two more test paths, [0, 4, 6] for prime path 19, and [0, 4, 4, 6] for prime path 14. The complete set of test paths is then: 1) 2) 3) 4) 5)
[0, 1, 2, 3, 1, 5, 6] [0, 1, 2, 3, 1, 2, 3, 1, 5, 6] [0, 1, 5, 6] [0, 4, 6] [0, 4, 4, 6]
This can be used as is, or optimized if the tester desires a smaller test set. It is clear that test path 2 tours the prime paths toured by test path 1, so 1 can be eliminated, leaving the four test paths identified informally earlier in this section. Simple algorithms can automate this process.
EXERCISES Section 2.2.1. 1. Redefine edge coverage in the standard way (see the discussion for node coverage). 2. Redefine complete path coverage in the standard way (see the discussion for node coverage). 3. Subsumption has a significant weakness. Suppose criterion Cstrong subsumes criterion Cweak and that test set Tstrong satisfies Cstrong and test set Tweak satisfies Cweak . It is not necessarily the case that Tweak is a subset of Tstrong . It is also not necessarily the case that Tstrong reveals a fault if Tweak reveals a fault. Explain these facts. 4. Answer questions (a)–(d) for the graph defined by the following sets: N = {1, 2, 3, 4} N0 = {1} N f = {4} E = {(1, 2), (2, 3), (3, 2), (2, 4)}
(a) Draw the graph. (b) List test paths that achieve node coverage, but not edge coverage. (c) List test paths that achieve edge coverage, but not edge Pair coverage. (d) List test paths that achieve edge pair coverage. 5. Answer questions (a)–(g) for the graph defined by the following sets: N = {1, 2, 3, 4, 5, 6, 7} N0 = {1} N f = {7} E = {(1, 2), (1, 7), (2, 3), (2, 4), (3, 2), (4, 5), (4, 6), (5, 6), (6, 1)} Also consider the following (candidate) test paths: t0 = [1, 2, 4, 5, 6, 1, 7] t1 = [1, 2, 3, 2, 4, 6, 1, 7] (a) Draw the graph. (b) List the test requirements for edge-pair coverage. (Hint: You should get 12 requirements of length 2). (c) Does the given set of test paths satisfy edge-pair coverage? If not, identify what is missing. (d) Consider the simple path [3, 2, 4, 5, 6] and test path [1, 2, 3, 2, 4, 6, 1, 2, 4, 5, 6, 1, 7]. Does the test path tour the simple path directly? With a sidetrip? If so, identify the sidetrip. (e) List the test requirements for node coverage, edge coverage, and prime path coverage on the graph. (f) List test paths that achieve node coverage but not edge coverage on the graph. (g) List test paths that achieve edge coverage but not prime path coverage on the graph. 6. Answer questions (a)–(c) for the graph in Figure 2.2. (a) Enumerate the test requirements for node coverage, edge coverage, and prime path coverage on the graph. (b) List test paths that achieve node coverage but not edge coverage on the graph. (c) List test paths that achieve edge coverage but not prime path coverage on the graph. 7. Answer questions (a)–(d) for the graph defined by the following sets: N = {0, 1, 2} N0 = {0} N f = {2} E = {(0, 1), (0, 2), (1, 0), (1, 2), (2, 0)} Also consider the following (candidate) paths: p0 = [0, 1, 2, 0] p1 = [0, 2, 0, 1, 2] p2 = [0, 1, 2, 0, 1, 0, 2] p3 = [1, 2, 0, 2] p4 = [0, 1, 2, 1, 2] (a) Which of the listed paths are test paths? Explain the problem with any path that is not a test path.
use (n 0 , n1 ) = { a, b } def (n 0 ) = { a,b }
use (n 0 , n2 ) = { a, b }
n2 use (n 2 ) = { a, b }
a=b
def (n 3) = { b }
Figure 2.11. A graph showing variables, def sets and use sets.
(b) List the eight test requirements for edge-pair coverage (only the length two subpaths). (c) Does the set of test paths (part a) above satisfy edge-pair coverage? If not, identify what is missing. (d) Consider the prime path [n2 , n0 , n2 ] and path p2 . Does p2 tour the prime path directly? With a sidetrip? 8. Design and implement a program that will compute all prime paths in a graph, then derive test paths to tour the prime paths. Although the user interface can be arbitrarily complicated, the simplest version will be to accept a graph as input by reading a list of nodes, initial nodes, final nodes, and edges.
2.2.2 Data Flow Criteria The next few testing criteria are based on the assumption that to test a program adequately, we should focus on the flows of data values. Specifically, we should try to ensure that the values created at one point in the program are created and used correctly. This is done by focusing on definitions and uses of values. A definition (def) is a location where a value for a variable is stored into memory (assignment, input, etc.). A use is a location where a variable's value is accessed. Data flow testing criteria use the fact that values are carried from defs to uses. We call these du-pairs (they are also known as definition-use, def-use, and du associations in the testing literature). The idea of data flow criteria is to exercise du-pairs in various ways. First we must integrate data flow into the existing graph model. Let V be a set of variables that are associated with the program artifact being modeled in the graph. Each node n and edge e is considered to define a subset of V; this set is called def(n) or def(e). (Although graphs from programs cannot have defs on edges, other software artifacts such as finite state machines can allow defs as side effects on edges.) Each node n and edge e is also considered to use a subset of V; this set is called use(n) or use(e). Figure 2.11 gives an example of a graph annotated with defs and uses. All variables involved in a decision are assumed to be used on the associated edges, so a and b are in the use set of all three edges (n0 , n1 ), (n0 , n2 ), and (n0 , n3 ).
An important concept when discussing data flow criteria is that a def of a variable may or may not reach a particular use. The most obvious reason that a def of a variable v at location li (a location could be a node or an edge) will not reach a use at location l j is because no path goes from li to l j . A more subtle reason is that the variable's value may be changed by another def before it reaches the use. Thus, a path from li to l j is def-clear with respect to variable v if for every node nk and every edge ek on the path, k = i and k = j, v is not in def(nk ) or in def(ek ). That is, no location between li and l j changes the value. If a def-clear path goes from li to l j with respect to v, we say that the def of v at li reaches the use at l j . For simplicity, we will refer to the start and end of a du-path as nodes, even if the definition or the use occurs on an edge. We discuss relaxing this convention later. Formally, a du-path with respect to a variable v is a simple path that is def-clear with respect to v from a node ni for which v is in def(ni ) to a node n j for which v is in use(n j ). We want the paths to be simple to ensure a reasonably small number of paths. Note that a du-path is always associated with a specific variable v, a du-path always has to be simple, and there may be intervening uses on the path. Figure 2.12 gives an example of a graph annotated with defs and uses. Rather than displaying the actual sets, we show the full program statements that are associated with the nodes and edges. This is common and often more informative to a human, but the actual sets are simpler for automated tools to process. Note that the parameters (subject and pattern) are considered to be explicitly defined by the first node in the graph. That is, the def set of node 1 is def(1) = {subject, pattern}. Also note that decisions in the program (for example, if subject[i Sub] == pattern[0]) result in uses of each of the associated variables for both edges in the decision. That is, use(4, 10) ≡ use(4,5) ≡ {subject, i Sub, pattern}. The parameter subject is used at node 2 (with a reference to its length attribute) and at edges (4, 5), (4, 10), (7, 8), and (7, 9), thus du-paths exist from node 1 to node 2 and from node 1 to each of those four edges. Figure 2.13 shows the same graph, but this time with the def and use sets explicitly marked on the graph.3 Note that node 9 both defines and uses the variable iPat. This is because of the statement iPat ++, which is equivalent to iPat = iPat+1. In this case, the use occurs before the def, so for example, a def-clear path goes from node 5 to node 9 with respect to iPat. The test criteria for data flow will be defined as sets of du-paths. This makes the criteria quite simple, but first we need to categorize the du-paths into several groups. The first grouping of du-paths is according to definitions. Specifically, consider all of the du-paths with respect to a given variable defined in a given node. Let the def-path set du(ni , v) be the set of du-paths with respect to variable v that start at node ni . Once we have clarified the notion of touring for dataflow coverage, we will define the All-Defs criterion by simply asking that at least one du-path from each def-path set be toured. Because of the large number of nodes in a typical graph, and the potentially large number of variables defined at each node, the number of def-path sets can be quite large. Even so, the coverage criterion that arises from the def-path groupings tends to be quite weak. Perhaps surprisingly, it is not helpful to group du-paths by uses, and so we will not provide a definition of "use-path" sets that parallels the definition of def-path sets given above.
subject, pattern are forwarded parameters NOTFOUND = -1 iSub = 0 rtnIndex = NOTFOUND isPat = false subjectLen = subject.length patternLen = pattern.length
iSub + patternLen - 1 < subjectLen && isPat = = false (iSub + patternLen - 1 >= subjectLen || isPat != false )
subject [iSub] == pattern [0]
(subject [iSub] != pattern [0])
rtnIndex = iSub isPat = true iPat = 1
iPat < patternLen iPat >= patternLen 7
subject[iSub + iPat] != pattern[iPat] 11
iSub++
return (rtnIndex) rtnIndex = NOTFOUND isPat = false;
subject[iSub + iPat] == pattern[iPat] 9
iPat++
Figure 2.12. A graph showing an example of du-paths.
The second, and more important, grouping of du-paths is according to pairs of definitions and uses. We call this the def-pair set. After all, the heart of data flow testing is allowing definitions to flow to uses. Specifically, consider all of the du-paths with respect to a given variable that are defined in one node and used in another (possibly identical) node. Formally, let the def-pair set du(ni , n j , v) be the set of dupaths with respect to variable v that start at node ni and end at node n j . Informally, a def-pair set collects together all the (simple) ways to get from a given definition to a given use. Once we have clarified the notion of touring for dataflow coverage, we will define the All-Uses criterion by simply asking that at least one du-path from
def(1) = { subject, pattern }
def(2) = {NOTFOUND, iSub, rtnIndex, isPat, subjectLen, patternLen } use(2) = {subject , pattern }
use (3,11) = use (3,4) = { iSub, patternLen, subjectLen, isPat } 4
use(4,10) = use(4,5) = { subject, iSub, pattern } 5
def(5) = { rtnIndex, isPat, iPat } use(5) = { iSub }
use(6,10)=use(6,7) ={ iPat, patternLen } 7
use(7,8)=use(7,9) ={ subject, pattern, iSub, iPat } 11
use(11) = { rtnIndex } def (10)={ iSub } use(10)={ iSub }
def(8) = { rtnIndex, isPat } use(8) = { NOTFOUND }
def (9)={ iPat } use(9)={ iPat }
Figure 2.13. Graph showing explicit def and use sets.
each def-pair set be toured. Since each definition can typically reach multiple uses, there are usually many more def-pair sets than def-path sets. In fact, the def-pair set for a def at node ni is the union of all the def-path sets for that def. More formally: du(ni , v) = ∪n j du(ni , n j , v). To illustrate the notions of def-path sets and def-pair sets, consider du-paths with respect to the variable iSub, which has one of its definitions in node 10 in Figure 2.13. There are du-paths with respect to iSub from node 10 to nodes 5 and 10, and to edges (3, 4), (3, 11), (4, 5), (4, 10), (7, 8), and (7, 9).
The def-path set for the use of isub at node 10 is: du(10, i Sub) = {[10, 3, 4], [10, 3, 4, 5], [10, 3, 4, 5, 6, 7, 8], [10, 3, 4, 5, 6, 7, 9], [10, 3, 4, 5, 6, 10], [10, 3, 4, 5, 6, 7, 8, 10], [10, 3, 4, 10], [10, 3, 11]} This def-path set can be broken up into the following def-pair sets: du(10, 4, iSub) du(10, 5, iSub) du(10, 8, iSub) du(10, 9, iSub) du(10, 10, iSub) du(10, 11, iSub)
= is{[10, 3, 4]} = {[10, 3, 4, 5]} = {[10, 3, 4, 5, 6, 7, 8]} = {[10, 3, 4, 5, 6, 7, 9]} = {[10, 3, 4, 5, 6, 10], [10, 3, 4, 5, 6, 7, 8, 10], [10, 3, 4, 10]} = {[10, 3, 11]}
Next, we extend the definition of tour to apply to du-paths. A test path p is said to du tour subpath d with respect to v if p tours d and the portion of p to which d corresponds is def-clear with respect to v. Depending on how one wishes to define the coverage criteria, one can either allow or disallow def-clear sidetrips with respect to v when touring a du-path. Because def-clear sidetrips make it possible to tour more du-paths, we define the dataflow coverage criteria given below to allow sidetrips where necessary. Now we can define the primary data flow coverage criteria. The three most common are best understood informally. The first requires that each def reaches at least one use, the second requires that each def reaches all possible uses, and the third requires that each def reaches all possible uses through all possible du-paths. As mentioned in the development of def-path sets and def-pair sets, the formal definitions of the criteria are simply appropriate selections from the appropriate set. For each test criterion below, we assume best effort touring (see Section 2.2.1), where sidetrips are required to be def-clear with respect to the variable in question. Criterion 2.9 All-Defs Coverage (ADC): For each def-path set S = du(n, v), T R contains at least one path d in S. Remember that the def-path set du(n, v) represents all def-clear simple paths from n to all uses of v. So All-Defs requires us to tour at least one path to at least one use. Criterion 2.10 All-Uses Coverage (AUC): For each def-pair set S = du(ni , n j , v), T R contains at least one path d in S. Remember that the def-pair set du(ni , n j , v) represents all the def-clear simple paths from a def of v at ni to a use of v at n j . So All-Uses requires us to tour at least one path for every def-use pair.4 Criterion 2.11 All-du-Paths Coverage (ADUPC): For each def-pair set S = du (ni , n j , v), T R contains every path d in S. The definition could also simply be written as "include every du-path." We chose the given formulation because it highlights that the key difference between All-Uses
def (0) = { X }
use (4) = { X }
All-defs 0-1-3-4
All-uses 0-1-3-4 0-1-3-5
All-du-paths 0-1-3-4 0-1-3-5 0-2-3-4 0-2-3-5
Figure 2.14. Example of the differences among the three data flow coverage criteria.
and All-du-Paths is a change in quantifier. Specifically, the "at least one du-path" directive in All-Uses is changed to "every path" in All-du-Paths. Thought of in terms of def-use pairs, All-Uses requires some def-clear simple path to each use, whereas All-du-Paths requires all def-clear simple paths to each use. To simplify the development above, we assumed that definitions and uses occurred on nodes. Naturally, definitions and uses can occur on edges as well. It turns out that the development above also works for uses on edges, so data flow on program flow graphs can be easily defined (uses on program flow graph edges are sometimes called "p-uses"). However, the development above does not work if the graph has definitions on edges. The problem is that a du-path from an edge to an edge is no longer necessarily simple, since instead of simply having a common first and last node, such a du-path now might have a common first and last edge. It is possible to modify the definitions to explicitly mention definitions and uses on edges as well as nodes, but the definitions tend to get messier. The bibliographic notes contain pointers for this type of development. Figure 2.14 illustrates the differences among the three data flow coverage criteria with the double-diamond graph. The graph has one def, so only one path is needed to satisfy all-defs. The def has two uses, so two paths are needed to satisfy all-uses. Since two paths go from the def to each use, four paths are needed to satisfy all-du-paths. Note that the definitions of the data flow criteria leave open the choice of touring. The literature uses various choices – in some cases demanding direct touring, and, in other cases, allowing def-clear sidetrips. Our recommendation is best-effort touring, a choice that, in contrast to the treatments in the literature, yields the desired subsumption relationships even in the case of infeasible test
requirements. From a practical perspective, best-effort touring also makes sense – each test requirement is satisfied as rigorously as possible.
2.2.3 Subsumption Relationships among Graph Coverage Criteria Recall from Chapter 1 that coverage criteria are often related to one another by subsumption. The first relation to note is that edge coverage subsumes node coverage. In most cases, this is because if we traverse every edge in a graph, we will visit every node. However, if a graph has a node with no incoming or outgoing edges, traversing every edge will not reach that node. Thus, edge coverage is defined to include every path of length up to 1, that is, of length 0 (all nodes) and length 1 (all edges). The subsumption does not hold in the reverse direction. Recall that Figure 2.6 gave an example test set that satisfied node coverage but not edge coverage. Hence, node coverage does not subsume edge coverage. We have a variety of subsumption relations among the criteria. Where applicable, the structural coverage relations assume best-effort touring. Because best-effort Touring is assumed, the subsumption results hold even if some test requirements are infeasible. The subsumption results for data flow criteria are based on three assumptions: (1) every use is preceded by a def, (2) every def reaches at least one use, and (3) for every node with multiple outgoing edges, at least one variable is used on each out edge, and the same variables are used on each out edge. If we satisfy All-Uses Complete Path Coverage CPC
Prime Path Coverage PPC All-du-Paths Coverage ADUPC
All-Uses Coverage AUC
All-Defs Coverage ADC
Edge-Pair Coverage EPC
Complete Round Trip Coverage CRTC
Edge Coverage EC
Simple Round Trip Coverage SRTC
Node Coverage NC
Figure 2.15. Subsumption relations among graph coverage criteria.
coverage, then we will have implicitly ensured that every def was used. Thus AllDefs is also satisfied and All-Uses subsumes All-Defs. Likewise, if we satisfy All-duPaths coverage, then we will have implicitly ensured that every def reached every possible use. Thus All-Uses is also satisfied and All-du-Paths subsumes All-Uses. Additionally, each edge is based on the satisfaction of some predicate, so each edge has at least one use. Therefore All-Uses will guarantee that each edge is executed at least once, so All-Uses subsumes edge coverage. Finally, each du-path is also a simple path, so prime path coverage subsumes Alldu-Paths coverage.5 This is a significant observation, since computing prime paths is considerably simpler than analyzing data flow relationships. Figure 2.15 shows the subsumption relationships among the structural and data flow coverage criteria.
EXERCISES Section 2.2.3. 1. Below are four graphs, each of which is defined by the sets of nodes, initial nodes, final nodes, edges, and defs and uses. Each graph also contains a collection of test paths. Answer the following questions about each graph. Graph I. N = {0, 1, 2, 3, 4, 5, 6, 7} N0 = {0} N f = {7} E = {(0, 1), (1, 2), (1, 7), (2, 3), (2, 4), (3, 2), (4, 5), (4, 6), (5, 6), (6, 1)} de f (0) = de f (3) = use(5) = use(7) = {x} Test Paths: t1 = [0, 1, t2 = [0, 1, t3 = [0, 1, t4 = [0, 1, t5 = [0, 1, t6 = [0, 1,
7] 2, 2, 2, 2, 2,
4, 4, 3, 3, 3,
7] 1, 6, 2, 6,
7] 1, 7] 4, 5, 6, 1, 7] 1, 2, 4, 5, 6, 1, 7]
Graph II. N = {1, 2, 3, 4, 5, 6} N0 = {1} N f = {6} E = {(1, 2), (2, 3), (2, 6), (3, 4), (3, 5), (4, 5), (5, 2)} de f (x) = {1, 3} use(x) = {3, 6} // Assume the use of x in 3 precedes the def Test Paths: t1 = [1, 2, 6] t2 = [1, 2, 3, 4, 5, 2, 3, 5, 2, 6] t3 = [1, 2, 3, 5, 2, 3, 4, 5, 2, 6] t4 = [1, 2, 3, 5, 2, 6]
Graph III. N = {1, 2, 3, 4, 5, 6} N0 = {1} N f = {6} E = {(1, 2), (2, 3), (3, 4), (3, 5), (4, 5), (5, 2), (2, 6)} de f (x) = {1, 4} use(x) = {3, 5, 6} Test Paths: t1 = [1, 2, 3, 5, 2, 6] t2 = [1, 2, 3, 4, 5, 2, 6]
Graph IV. N = {1, 2, 3, 4, 5, 6} N0 = {1} N f = {6} E = {(1, 2), (2, 3), (2, 6), (3, 4), (3, 5), (4, 5), (5, 2)} de f (x) = {1, 5} use(x) = {5, 6} // Assume the use of x in 5 precedes the def Test Paths: t1 = [1, 2, 6] t2 = [1, 2, 3, 4, 5, 2, 3, 5, 2, 6] t3 = [1, 2, 3, 5, 2, 3, 4, 5, 2, 6]
(a) Draw the graph. (b) List all of the du-paths with respect to x. (Note: Include all du-paths, even those that are subpaths of some other du-path). (c) For each test path, determine which du-paths that test path tours. For this part of the exercise, you should consider both direct touring and sidetrips. Hint: A table is a convenient format for describing this relationship. (d) List a minimal test set that satisfies all-defs coverage with respect to x. (Direct tours only.) Use the given test paths. (e) List a minimal test set that satisfies all-uses coverage with respect to x.
if (x < y) { y = 0; x = x + 1; } else { x = y; }
n0 x> y
x 0) { w++; // node 2 } else { w=2*w; // node 3 } // node 4 (no executable statement) if (y = N) 27. state = lineBreak; 28. else if (c == CR) 29. state = crFound; 30. else if (c == ' ') 31. state = betweenWord; 32. else 33. state = inWord; 34. switch (state) 35. { 36. case betweenWord:
37. lastSpace = i; 38. break; 39. 40. case lineBreak: 41. SArr [lastSpace] = CR; 42. col = i-lastSpace; 43. break; 44. 45. case crFound: 46. if (i+1 < S.length() && SArr[i+1] == CR) 47. { 48. i++; // Two CRs => hard return 49. col = 1; 50. } 51. else 52. SArr[i] = ''; 53. break; 54. 55. case inWord: 56. default: 57. break; 58. } // end switch 59. i++; 60. } // end while 61. S = new String (SArr) + CR; 62. return (S); 63. }
(a) Draw the control flow graph for the fmtRewrap() method. (b) For fmtRewrap(), find a test case such that the corresponding test path visits the edge that connects the beginning of the while statement to the S = new String(SArr) + CR; statement without going through the body of the while loop. (c) Enumerate the test requirements for node coverage, edge coverage, and prime path coverage for the graph for fmtRewrap(). (d) List test paths that achieve node coverage but not edge coverage on the graph. (e) List test paths that achieve edge coverage but not prime path coverage on the graph. 7. Use the following method printPrimes() for questions a–f below. 1. /** ***************************************************** 2. * Finds and prints n prime integers 3. * Jeff Offutt, Spring 2003 4. ********************************************************* */ 5. private static void printPrimes (int n) 6. {
7. int curPrime; // Value currently considered for primeness 8. int numPrimes; // Number of primes found so far. 9. boolean isPrime; // Is curPrime prime? 10. int [] primes = new int [MAXPRIMES]; // The list of prime numbers. 11. 12. // Initialize 2 into the list of primes. 13. primes [0] = 2; 14. numPrimes = 1; 15. curPrime = 2; 16. while (numPrimes < n) 17. { 18. curPrime++; // next number to consider ... 19. isPrime = true; 20. for (int i = 0; i 0) 20 if (a > 0) 7 m = 4; 21 e = 2*b+d; 8 if (x > 5) 22 else 9 n = 3*m; 23 e = b+d; 10 else 24 return (e); 11 n = 4*m; 25 } 12 int o = takeOut (m, n); 13 System.out.println ("o is: " + o); 14 }
(a) Give all call sites using the line numbers given. (b) Give all pairs of last-defs and first-uses. (c) Provide test inputs that satisfy all-coupling-uses (note that trash() only has one input).
2.5 GRAPH COVERAGE FOR SPECIFICATIONS Testers can also use software specifications as sources for graphs. The literature presents many techniques for generating graphs and criteria for covering those graphs, but most of them are in fact very similar. We begin by looking at graphs based on sequencing constraints among methods in classes, then graphs that represent state behavior of software.
2.5.1 Testing Sequencing Constraints We pointed out in Section 2.4.1 that call graphs for classes often wind up being disconnected, and in many cases, such as with small abstract data types (ADTs), methods in a class share no calls at all. However, the order of calls is almost always constrained by rules. For example, many ADTs must be initialized before being used, we cannot pop an element from a stack until something has been pushed onto it, and we cannot remove an element from a queue until an element has been put on it. These rules impose constraints on the order in which methods may be called. Generally, a sequencing constraint is a rule that imposes some restriction on the order in which certain methods may be called. Sequencing constraints are sometimes explicitly expressed, sometimes implicitly expressed, and sometimes not expressed at all. Sometimes they are encoded as a precondition or other specification, but not directly as a sequencing condition. For example, consider the following precondition for DeQueue(): public int DeQueue () { // Pre: At least one element must be on the queue. . : public EnQueue (int e) { // Post: e is on the end of the queue.
Although it is not said explicitly, a wise programmer can infer that the only way an element can "be on the queue" is if EnQueue() has previously been called. Thus, an implicit sequencing constraint occurs between EnQueue() and DeQueue(). Of course, formal specifications can help make the relationships more precise. A wise tester will certainly use formal specifications when available, but a responsible tester must look for formal relationships even when they are not explicitly stated. Also, note that sequencing constraints do not capture all the behavior, but only abstract certain key aspects. The sequence constraint that EnQueue() must be called
write (t)
open (F)
S5 S4
close ()
write (t) S6 S6
Figure 2.32. Control flow graph using the File ADT.
before DeQueue() does not capture the fact that if we only EnQueue() one item, and then try to DeQueue() two items, the queue will be empty. The precondition may capture this fact, but usually not in a formal way that automated tools can use. This kind of relationship is beyond the ability of a simple sequencing constraint but can be dealt with by some of the state behavior techniques in the next section. This relationship is used in two places during testing. We illustrate them with a small example of a class that encapsulates operations on a file. Our class FileADT will have three methods:
open (String fName) // Opens the file with the name fName close (String fName) // Closes the file and makes it unavailable for use write (String textLine) // Writes a line of text to the file This class has several sequencing constraints. The statements use "must" and "should" in very specific ways. When "must" is used, it implies that violation of the constraint is a fault. When "should" is used, it implies that violation of the constraint is a potential fault, but not necessarily. 1. An open(F) must be executed before every write(t) 2. An open(F) must be executed before every close() 3. A write(t) must not be executed after a close() unless an open(F) appears in between 4. A write(t) should be executed before every close() 5. A close() must not be executed after a close() unless an open(F) appears in between 6. An open(F) must not be executed after an open(F) unless a close() appears in between Constraints are used in testing in two ways to evaluate software that uses the class (a "client"), based on the CFG of Section 2.3.1. Consider the two (partial) CFGs in Figure 2.32, representing two units that use FileADT. We can use this graph to test the use of the FileADT class by checking for sequence violations. This can be done both statically and dynamically.
Static checks (not considered to be traditional testing) proceed by checking each constraint. First consider the write(t) statements at nodes 2 and 5 in graph (a). We can check to see whether paths exist from the open(F) at node 1 to nodes 2 and 5 (constraint 1). We can also check whether a path exists from the open(F) at node 1 to the close() at node 6 (constraint 2). For constraints 3 and 4, we can check to see if a path goes from the close() at node 6 to any of the write(t) statements, and see if a path exists from the open(F) to the close() that does not go through at least one write(t). This will uncover one possible problem, the path [1, 3, 4, 6] goes from an open(F) to a close() with no intervening write(t) calls. For constraint 5, we can check if a path exists from a close() to a close() that does not go through an open(F). For constraint 6, we can check if a path exists from an open(F) to an open(F) that does not go through a close(). This process will find a more serious problem with graph (b) in 2.32. A path exists from the close() at node 7 to the write(t) at node 5 and to the write(t) at node 4. While this may seem simple enough not to require formalism for such small graphs, this process is quite difficult with large graphs containing dozens or hundreds of nodes. Dynamic testing follows a slightly different approach. Consider the problem in graph (a) where no write() appears on the possible path [1, 3, 4, 6]. It is quite possible that the logic of the program dictates that the edge (3, 4) can never be taken unless the loop [3, 5, 3] is taken at least once. Because deciding whether the path [1, 3, 4, 6] can be taken or not is formally undecidable, this situation can be checked only by dynamic execution. Thus we generate test requirements to try to violate the sequencing constraints. For the FileADT class, we generate the following sets of test requirements: 1. Cover every path from the start node to every node that contains a write(t) such that the path does not go through a node containing an open(F). 2. Cover every path from the start node to every node that contains a close() such that the path does not go through a node containing an open(F). 3. Cover every path from every node that contains a close() to every node that contains a write(t) such that the path does not contain an open(F). 4. Cover every path from every node that contains an open(F) to every node that contains a close() such that the path does not go through a node containing a write(t). 5. Cover every path from every node that contains an open(F) to every node that contains an open(F). Of course, all of these test requirements will be infeasible in well written programs. However, any tests created as a result of these requirements will almost certainly reveal a fault if one exists.
2.5.2 Testing State Behavior of Software The other major method for using graphs based on specifications is to model state behavior of the software by developing some form of finite state machine (FSM). Over the last 25 years, many suggestions have been made for creating FSMs and how to test software based on the FSM. The topic of how to create, draw, and interpret a
FSM has filled entire textbooks, and authors have gone into great depth and effort to define what exactly goes into a state, what can go onto edges, and what causes transitions. Rather than using any particular notation, we choose to define a very generic model for FSMs that can be adapted to virtually any notation. These FSMs are essentially graphs, and the graph testing criteria already defined can be used to test software that is based on the FSM. One of the advantages of basing tests on FSMs is that huge numbers of practical software applications are based on a FSM model or can be modeled as FSMs. Virtually all embedded software fits in this category, including software in remote controls, household appliances, watches, cars, cell phones, airplane flight guidance, traffic signals, railroad control systems, network routers, and factory automation. Indeed, most software can be modeled with FSMs, the primary limitation being the number of states needed to model the software. Word processors, for example, contain so many commands and states that modeling them as FSMs is probably impractical. Creating FSMs often has great value. If the test engineer creates a FSM to describe existing software, he or she will almost certainly find faults in the software. Some would even argue the converse; if the designers created FSMs, the testers should not bother creating them because problems will be rare. FSMs can be annotated with different types of actions, including actions on transitions, entry actions on nodes, and exit actions on nodes. Many languages are used to describe FSMs, including UML statecharts, finite automata, state tables (SCR), and petri nets. This book presents examples with basic features that are common to many languages. It is closest to UML statecharts, but not exactly the same. A finite state machine is a graph whose nodes represent states in the execution behavior of the software and edges represent transitions among the states. A state represents a recognizable situation that remains in existence over some period of time. A state is defined by specific values for a set of variables; as long as those variables have those values the software is considered to be in that state. (Note that these variables are defined at the design modeling level and may not necessarily correspond to variables in the software.) A transition is thought of as occurring in zero time and usually represents a change to the values of one or more variables. When the variables change, the software is considered to move from the transition's pre-state (predecessor) to its post-state (successor). (If a transition's pre-state and post-state are the same, then values of state variables will not change.) FSMs often define preconditions or guards on transitions, which define values that specific variables must have for the transition to be enabled, and triggering events, which are changes in variable values that cause the transition to be taken. A triggering event "triggers" the change in state. For example, the modeling language SCR calls these WHEN conditions and triggering events. The values the triggering events have before the transition are called before-values, and the values after the transition are called after-values. When graphs are drawn, transitions are often annotated with the guards and the values that change. Figure 2.33 illustrates this model with a simple transition that opens an elevator door. If the elevator button is pressed (the triggering event), the door opens only if the elevator is not moving (the precondition, elevSpeed = 0). Given this type of graph, many of the previous criteria can be defined directly. Node coverage requires that each state in the FSM be visited at least once and is
Graph Coverage open door
Closed pre-state
post-state pre: elevSpeed = 0 trigger: openButton = pressed
Figure 2.33. Elevator door open transition.
called state coverage. Edge coverage is applied by requiring that each transition in the FSM be visited at least once, which is called transition coverage. The edge-pair coverage criterion was originally defined for FSMs and is also called transition-pair and two-trip. The data flow coverage criteria are a bit more troublesome for FSMs. In most formulations of FSMs, nodes are not allowed to have defs or uses of variables. That is, all of the action is on the transitions. Unlike with code-based graphs, different edges from the same node in a FSM need not have the same set of defs and uses. In addition, the semantics of the triggers imply that the effects of a change to the variables involved are felt immediately by taking a transition to the next state. That is, defs of triggering variables immediately reach uses. Thus, the All-Defs and All-Uses criteria can only be applied meaningfully to variables involved in guards. This also brings out a more practical problem, which is that the FSMs do not always model assignment to all variables. That is, the uses are clearly marked in the FSM, but defs are not always easy to find. Because of these reasons, few attempts have been made to apply data flow criteria to FSMs.
Deriving Finite State Machine Graphs One of the difficult parts of applying graph techniques to FSMs is deriving the FSM model of the software in the first place. As we said earlier, FSM models of the software may already exist, or may not. If not, the tester is likely to dramatically increase his or her understanding of the software by deriving the FSMs. However, it is not necessarily obvious how to go about deriving a FSM, so we offer some suggestions. This is not a complete tutorial on constructing FSMs; indeed, a number of complete texts exist on the subject and we recommend that the interested reader study these elsewhere. This section offers some simple and straightforward suggestions to help readers who are unfamiliar with FSMs get started and avoid some of the more obvious mistakes. We offer the suggestions in terms of a running example, the class Stutter in Figures 2.34 and 2.35. Class Stutter checks each adjacent pair of words in a text file and prints a message if a pair is identical. The second author originally wrote it to edit his papers and find a common mistake mistake. Class Stutter has a main method and three support methods. When left to their own devices, students will usually pick one of four strategies for generating FSMs from code. Each of these is discussed in turn. 1. 2. 3. 4.
Combining control flow graphs Using the software structure Modeling state variables Using the implicit or explicit specifications
1. Combining control flow graphs: For programmers who have little or no knowledge of FSMs, this is often the most natural approach to deriving FSMs. Our experience
Coverage Criteria /** ***************************************************** // Stutter checks for repeat words in a text file. // It prints a list of repeat words, by line number. // Stutter will accept standard input or a list // of file names. // Jeff Offutt, June 1989 (in C), Java version March 2003 //********************************************************* */ class Stutter { // Class variables used in multiple methods. private static boolean lastdelimit = true; private static String curWord = "", prevWord = ""; private static char delimits [] = {'', ' ', ',', '.', '!', '-', '+', '=', ';', ':', '?', '&', '{', '}', '\\'}; // First char in list is a tab //************************************************ // main parses the arguments, decides if stdin // or a file name, and calls Stut(). //************************************************ public static void main (String[] args) throws IOException { String fileName; FileReader myFile; BufferedReader inFile = null; if (args.length == 0) { // no file, use stdin inFile = new BufferedReader (new InputStreamReader (System.in)); } else { fileName = args [0]; if (fileName == null) { // no file name, use stdin inFile = new BufferedReader (new InputStreamReader (System.in)); } else { // file name, open the file. myFile = new FileReader (fileName); inFile = new BufferedReader (myFile); } } stut (inFile); } //************************************************ // Stut() reads all lines in the input stream, and // finds words. Words are defined as being surrounded // by delimiters as defined in the delimits[] array. // Every time an end of word is found, checkDupes() // is called to see if it is the same as the // previous word. //************************************************ private static void stut (BufferedReader inFile) throws IOException { String inLine; char c; int linecnt = 1;
Figure 2.34. Stutter – Part A.
has been that the majority of students will use this approach if not guided away from it. A control flow graph-based FSM for class Stutter is given in Figure 2.36. The graph in Figure 2.36 is not a FSM at all, and this is not the way to form graphs from software. This method has several problems, the first being that the nodes are not states. The methods must return to the appropriate callsites, which means that the graphs contain built-in nondeterminism. For example, in
Graph Coverage while ((inLine = inFile.readLine()) != null) { // For each line for (int i=0; i= 3 tries ]
Eject Card
Confiscate Card
[ invalid PIN ]
Prompt for Transaction
Check PIN
[ valid PIN ]
[ select Select Acct # withdraw ] [ select balance or transfer ]
[ valid account ] [ invalid account ]
[ not exceeded ]
[ daily amount exceeded ]
Print Welcome Message
[ card not expired ]
[card lost ]
[card not lost ]
[ sufficient funds ]
[ insufficient funds ]
[ ATM out of funds ] [ not out of funds ]
Dispense Cash
Figure 2.42. Activity graph for ATM withdraw funds.
obvious data definition-use pairs. This means that data flow coverage criteria are not applicable. The two criteria that are most obviously applicable to use case graphs are node coverage and edge coverage. Test case values are derived from interpreting the nodes and predicates as inputs to the software. One other criterion for use case graphs is based on the notion of "scenarios."
2.6.1 Use Case Scenarios A use case scenario is an instance of, or a complete path through, a use case. A scenario should make some sense semantically to the users and is often derived when the use cases are constructed. If the use case graph is finite (as is usually the case), then it is possible to list all possible scenarios. However, domain knowledge can be used to reduce the number of scenarios that are useful or interesting from either a modeling or test case perspective. Note that specified path coverage, defined at the beginning of this chapter, is exactly what we want here. The set S for specified path coverage is simply the set of all scenarios. If the tester or requirements writer chooses all possible paths as scenarios, then specified path coverage is equivalent to complete path coverage. The scenarios are chosen by people and they depend on domain knowledge. Thus it is not guaranteed that specified path coverage subsumes edge coverage or node coverage. That is, it is possible to choose a set of scenarios that do not include every edge. This would probably be a mistake, however. So, in practical terms, specified path coverage can be expected to cover all edges.
EXERCISES Section 2.6. 1. Construct two separate use cases and use case scenarios for interactions with a bank automated teller machine. Do not try to capture all the functionality of the ATM into one graph; think about two different people using the ATM and what each one might do. Design test cases for your scenarios.
2.7 REPRESENTING GRAPHS ALGEBRAICALLY While we typically think of graphs as circles and arrows, they can be represented in various nonpictorial ways. One useful way is an algebraic representation, which can be manipulated using standard algebraic operations and converted to regular expressions. These operations can then be used as a basis for testing the software and to answer various questions about the graphs. The first requirement is that each edge have a unique label or name. The edge names can come from labels that are already associated with the edges, or can be added specifically for the algebraic representation. This book assumes the labels are unique lower case letters. The multiplicative operator in graph algebra is concatenation; if edge a is followed by edge b, their product is ab (the operator '*' is not written explicitly). The additive operator is selection; if either edge a or edge b can be taken, their sum is a + b. Concatenating edges together forms a path, so a sequence of edges is called a path product. A path expression contains path products and zero or more '+' operators. Thus, every path product is a path expression. Note that an edge label is a special case of a path product with no multiplication, and a path product is a special case of a path expression with no addition. Path expressions are sometimes represented by upper case letters, for example, A = ab. Figure 2.43 shows three example graphs drawn from the double-diamond graph, the loop touring graph, and the Stutter example from previous sections. Figure 2.43(a) has exactly four paths, all of which are shown. Figure 2.43(b) and (c) include loops, so not all paths are shown. In graph algebra, loops are best represented using exponents. If an edge, path product, or path expression can be repeated, then it is labeled with an exponent. Therefore, a 2 = aa, a 3 = aaa, and a ∗ = aa · · · a, that is, an arbitrary number of repetitions. As a special case, an empty, or zero length path, can be represented by a 0 = λ. This makes λ the multiplicative identity, so aλ = a, or more generally, Aλ = A. Representing paths or partial paths with upper case letters makes for convenient manipulation. For example, we can take some partial path expressions from Figure 2.43(b) above: A B C AB C3
ab eg cd abeg cdcdcd
n2 c
d n3
abdfhj abdgij acefhj acegij
(a) A double diamond graph with edge labels
n5 abeg abcfg abcdeg
(b) A graph with a loop f a
n0 b
afk agfk bchk bdik bejk
n5 j i
(c) The first FSM for stutter
Figure 2.43. Examples of path products.
AC 2 B = ab(cd)2 eg = abcdcdeg D = be + bc f Unlike standard algebra, path products are not commutative. That is, AB = BA. They are, however, associative, so A(BC) = (AB)C = ABC. All paths in the graph in Figure 2.43(a) above can be represented by the expression: abdfhj + abdgij + acefhj + acegij. Paths that are summed can be considered to be independent or parallel paths. So path summation is both commutative and associative, that is, A + B = B + A, (A + B) + C = A + (B + C) = A + B + C. With this basis, we can start applying standard algebraic laws. Both the distributive law and absorption rule can be applied. A(B + C) = AB + AC (distributive) (B + C)D = BD + C D (distributive) A+ A = A (absorption rule)
We also have two more shorthand notations for repetition or loops. If a loop has to be taken at least once (for example, a repeat-until structure), then the '+' exponent is used. That is, AA∗ = A+ . We can also put bounds on repetition if a loop has a definite bound (for example, a for loop). This is done with an underscore: A3 = A0 + A1 + A2 + A3 , or more generally, An = A0 + A1 + · · · + An . It is sometimes helpful to bound the number of iterations on both ends – that is, at least m and at most n iterations are possible. To do this, we introduce the notation Am−n = Am + Am+1 + · · · + An . The absorption rule can be used to combine the exponent notations in several ways. This is used to simplify path expressions as follows: An + Am = An Am = An A∗ = n + A A = A∗ A+ =
Amax (n,m) An+m A∗ An = A∗ A+ An = A+ A+ A∗ = A+
The multiplicative identity operator, λ, can also be used to simplify path expressions. λ+λ = λA = = λn λ+ + λ =
λ Aλ = A λn = λ∗ = λ+ = λ λ∗ = λ
We also need an additive identity. We will use φ to represent the set of paths that contains no paths (not even the empty path λ). Mathematically, any path expression added to φ is just that path expression. The additive φ can be thought of as "blocking" the paths in the graph, therefore making a null path. A+φ = φ+ A= A Aφ = φA = φ = λ + φ + φ2 + · · · = λ φ∗ Figure 2.44 shows a small graph that has a null path. If we list all paths from node n0 to n3 , we get the path expression bc + aφ = bc. A special case is the path expression A + λ. This situation is illustrated in Figure 2.45. The complete path expression is (A + λ)B, or AB + λB, or AB + B. Thus, A + λ cannot be reduced. n1 a n0 b n2
Figure 2.44. Null path that leads to additive identity φ.
A n0
Figure 2.45. A or lambda.
2.7.1 Reducing Graphs to Path Expressions Now that we have the basic tools, we can see how to go about reducing arbitrary graphs to path expressions. The process given here is not a strict algorithm as it requires some thought and decisions, but is good enough to be used by human testers. As far as we know, this process has not been automated and implemented in a tool; however, it is a special case of the common technique of constructing regular expressions from deterministic FSMs. The process is illustrated on the graph shown in Figure 2.46. Step 1: First we combine all sequential edges, multiplying the edge labels. More formally, for any node that has only one incoming edge and one outgoing edge, eliminate the node, combine the two edges, and multiply their path expressions. Applying this step to the graph in Figure 2.46 combines edges h and i, giving the graph shown in Figure 2.47. Step 2: Next combine all parallel edges, adding the edge labels. More formally, for any pair of edges that have the same source and target nodes, combine the edges into one edge, and add their path expressions. The graph in Figure 2.47 contains one such pair of edges, b and c, so they are combined to yield b + c, giving the graph shown in Figure 2.48. Step 3: Remove self-loops (from a node to itself) by creating a new "dummy" node with an incoming edge that uses the exponent operator '*', then merging the three edges with multiplication. More formally, for any node n1 that has an edge to itself with label X, and incoming edge A and outgoing edge B, remove the edge with label X, and add a new node n 1 and an edge with label X ∗ . Then combine the three edges A, X ∗ , and B into one edge AX ∗ B (eliminating nodes n1 and n 1 ). The graph in Figure 2.48 contains one self-loop on node n3 with label e. The edge is first replaced with node n 3 and an edge from n3 to n 3 with label e∗ (as shown in Figure 2.49(a)), then the edges labeled d, e∗ and f are combined, as shown in Figure 2.49(b). Step 4: Now the tester starts choosing nodes to remove. Select a node that is not the initial or final node. Replace it by inserting edges from all predecessors to all successors, multiplying the path expressions from the incoming to the outgoing edges. Figure 2.50 illustrates this with a node that has two incoming and two outgoing edges. e
b n0
Figure 2.46. Example graph to show reduction to path expressions.
Graph Coverage e
Figure 2.47. After step 1 in path expression reduction. e n0
b+c
Figure 2.48. After step 2 in path expression reduction.
n3 e*
n'3
g (a) After inserting dummy node n0
de*f
g (b) After combining edges
BC n5
Figure 2.50. Removing arbitrary nodes.
bde*f + cde*f
gde*f
Figure 2.51. Eliminating node n2 .
abde*f + acde*f
Figure 2.52. Removing sequential edges.
(gde*f)*
Figure 2.53. Removing self-loop edges.
Node n2 in Figure 2.49(b) has two incoming edges and one outgoing edge. Edges (n1 , n2 ) and (n2 , n4 ) become edge (n1 , n4 ), with the two path expressions multiplied, and edges (n4 , n2 ) and (n2 , n4 ) become a self-loop (n4 , n4 ), with the two path expressions multiplied. The resulting graph is shown in Figure 2.51. Steps 1 through 4 are repeated until only one edge remains in the graph. Applying step 1 (combining sequential edges) again to the graph in Figure 2.51 yields the graph shown in Figure 2.52. Applying step 2 (combining parallel edges) again is skipped because the graph in Figure 2.52 has no parallel edges. Applying step 3 (removing self-loops) again to the graph in Figure 2.52 removes the self-loop on node n4 , yielding the graph shown in Figure 2.53. The final graph (and regular expression) in our example is shown in Figure 2.54.
2.7.2 Applications of Path Expressions Now that the mathematical preliminaries are out of the way, it is fair to ask what do we do with these path expressions? Path expressions are abstract, formal representations of graphs. As such, they can be manipulated to give us information about the graphs they represent. This section presents several applications of path expressions.
2.7.3 Deriving Test Inputs The most direct way to use path expression representations of graphs is to define covering test cases. Each path, that is, each path product, defined by the path expression should be executed, with an appropriate limitation on loops. This is a form of specified path coverage (SPC). If an unbounded exponent ('*') appears in the path expression, it can be replaced with one or more reasonably chosen constant values, then a complete list of paths can be written out. This technique will ensure (that is, subsume) node coverage and edge coverage on the graph. The final path expression for the example in Figures 2.46 through 2.54 is abde∗ f (gde∗ f )∗ hi + acde∗ f (gde∗ f )∗ hi. This expression has two separate path products, and the exponents can be replaced (arbitrarily) by the constant 5. This results in the following two test requirements: abde5 f (gde5 f )5 hi and acde5 f (gde5 f )5 hi.
abde*f(gde*f)*hi + acde*f(gde*f)*hi
Figure 2.54. Final graph with one path expression.
2.7.4 Counting Paths in a Flow Graph and Determining Max Path Length It is sometimes useful to know the number of paths in a graph. This can be used as a simplistic complexity measure or as a rough estimation of the number of tests needed to cover the graph. The path expressions allow this computation with straightforward arithmetic, yielding a reasonable approximation for the maximum number of paths. As discussed earlier, whenever a graph has a cycle, theoretically the graph has an infinite number of paths. However, some graphs have no cycles, and domain knowledge can be used to put appropriate bounds on the number of iterations. The bound may be a true maximum number of iterations, or it may represent a tester's assumption that executing the loop "N times" is enough. The first step is to label each edge with an edge weight. For most edges, the edge weight is one. If the edge represents an expensive operation, such as a method call or external process, the edge weight should be the approximate weight of that operation (for example, the number of paths in the method). If the edge represents a cycle, mark it with the maximum number of iterations possible (the cycle weight). It is possible that this number is infinite, which means the maximum number of paths in the graph is infinite. It is important that not all edges in a cycle be labeled with the cycle weight. Only one edge per each cycle should be labeled. Sometimes, which edge to label is obvious, other times the tester must choose an appropriate edge, taking care not to omit a cycle or label a cycle twice. Consider graphs (b) and (c) in Figure 2.43. It should be clear that the cycle weight should be placed on edge d in graph (b). Cycle weights should also be placed on edges h, i, and j in graph (c), and on both edges f and g. Edge f will always occur on any path that includes edge g, so it is easy to forget one of those cycle weights; however, they represent separate cycles. Sometimes we want to separately annotate a loop to indicate how many times it can be taken. The notation "(0–10)" means that the loop can be taken 0 to 10 times inclusive. Note that this notation is not the same as the edge weight. Next compute the path expression for the graph and substitute the weights into the path expression. The operators are used as one might expect. If the path expression is A + B, the substitution is WA + WB. If the path expression is AB, the substitution is WA ∗ WB. If the path expression is An , the substitution is the sum n WAi . If the path expression is Am−n , the substitution is the summation mation i=0 n i i=m WA . Figure 2.55 shows a simple graph with edge labels and edge weights. As indicated on edge d, the loop can be taken 0 to 2 times inclusive, and the edge weight for d is one. The resulting path expression is a(b + c)(d(b + c))0−2 e. b n0
Figure 2.55. Graph example for computing maximum number of paths.
The maximum number of paths can then be calculated by substituting the appropriate value for each edge label into the path expression. 1 ∗ (1 + 1) ∗ (1 ∗ (1 + 1))0−2 ∗ 1 = 1 ∗ 2 ∗ 20−2 ∗ 1 2 = 2 ∗ i=0 2i ∗ 1 0 = 2 ∗ (2 + 21 + 22 ) ∗ 1 = 2 ∗ (1 + 2 + 4) ∗ 1 =2∗7∗1 = 14 The length of the longest path in the graph can also be found. If the path expression is A + B, the substitution is max(WA , WB). If the path expression is AB, the substitution is WA + WB. If the path expression is An , the substitution is n ∗ WA . So the length of the longest path in the graph in Figure 2.55 is 1 + max(1, 1) + 2 ∗ (1 + max(1, 1)) + 1 = 7. It is important to remember that these analyses do not include a feasibility analysis. Some paths may be infeasible, so this should be interpreted as an upper, or conservative, bound on the number of paths.
2.7.5 Minimum Number of Paths to Reach All Edges A related question is how many paths have to be traversed to reach all edges. The process is very similar to counting the maximum number of paths and uses the same edge weights, but the computation is slightly different. Specifically, if the path expression is A + B, the substitution is WA + WB. However, if the path expression is AB, the substitution is max(WA , WB). If the path expression is An , the substitution requires some judgment from the tester and is either 1 or WA . If it is reasonable to assume that all paths through the loop can be taken during one test case, the value should be 1. If not, however, the value should be the weight of the loop, WA . The second assumption is more conservative and leads to a higher number. Again consider the graph in Figure 2.55. Assume that if the edge d is taken, the same edge that preceded it must then be taken. That is, if b is taken, then d, the logic of the graph dictates that b must be taken again. This means that we must use the conservative estimate for the loop, yielding 1 ∗ (2) ∗ (1 ∗ (2))2 ∗ 1 = 1 ∗ (2) ∗ (1 ∗ 2) ∗ 1 = max(1, 2, 1, 2, 1) =2 A visual inspection of the graph can confirm that all edges can be reached with two traversals of the graph.
2.7.6 Complementary Operations Analysis The last application of path expressions is not a counting application, but an analysis that looks for anomalies that may lead to mistakes. It is based on the idea of "complementary operations." Two operations are complementary if their behaviors negate each other, or one must be done before the other. Examples include push
Graph Coverage C
C n0
Figure 2.56. Graph example for complementary path analysis.
and pop in stacks, enqueue and dequeue in queues, get and dispose for memory, and open and close for files. The process starts with the path expression for a graph, except instead of edge weights, each edge is marked with one of the following three labels: 1. C – Creator operation (push, enqueue, etc.) 2. D – Destructor operation (pop, dequeue, etc.) 3. 1 – Neither a creator nor a destructor The path expression multiplicative and additive operators are replaced with the following two tables7 : * C D 1
C C2 DC C
D 1 D2 D
1 C D 1
+ C D 1
C C D+ C 1+C
D C+D D 1+ D
1 C+1 D+ 1 1
Note the differences from the usual algebra defined on integers. C ∗ D reduces to 1, C + C reduces to C, and D + D reduces to D. Consider the graph in Figure 2.56. Edges are marked with C, D or 1, and its initial path expression is C(C + 1)C(C + D)1(D(C + D)1)n 1. The algebraic rules are used to rewrite this as (CCCC + CCC D + CCC + CC D)(DC + DD)n . The two tables above can be used to further reduce the path expression to (CCCC + CC + CCC + C)(DC + DD)n . The first question to ask of this path expression is "is it possible to have more destruct operations than creates?" The answer is yes, and some expressions are CCC D(DD)n , n > 1 CC D(DD)n , n > 0 CCC(DDDC DD) Another question is "is it possible to have more create operations than destructs?" Again, the answer is yes, and some expressions are: CCCC CC D(DC)n , ∀n Each yes answer represents a specification for a test that is likely to cause anomalous behavior.
EXERCISES Section 2.7. 1. Derive and simplify the path expressions for the three graphs in Figure 2.43. 2. Derive and simplify the path expression for the flow graph in Figure 2.12. Assign reasonable cycle weights and compute the maximum number of paths in the graph and the minimum number of paths to reach all edges.
3. The graph in Figure 2.10 was used as an example for prime test paths. Add appropriate edge labels to the graph, then derive and simplify the path expressions. Next add edge weights of 1 for non-cycle edges and 5 for cycle edges. Then compute the maximum number of paths in the graph and the minimum number of paths to reach all edges. This graph has 25 prime paths. Briefly discuss the number of prime paths with the maximum number of paths and consider the effect of varying the cycle weight on the maximum number of paths. 4. Section 2.5 presented four different versions of a FSM for Stutter. Derive and simplify the path expressions for each of the four variations, then compute the maximum number of paths and the minimum number of paths to reach all edges in each. Discuss how the different numbers affect testing. 5. Perform complementary operations analysis on the graph in Figure 2.32. Assume complementary operators of open and close. 6. Derive and simplify the path expressions for the activity graph in Figure 2.42. The only loop in that graph has a bound of 3 tries. Use that to compute the maximum number of paths and the minimum number of paths to reach all edges. Discuss the relationship between the scenarios for that graph and the terms in the path expression. 7. Answer questions (a)–(c) for the graph defined by the following sets: N = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10} N0 = {1} N f = {10} E = {(1, 2, a), (2, 3, b), (2, 4, c), (3, 5, d), (4, 5, e), (5, 6, f ), (5, 7, g), (6, 6, h (1 − 4)), (6, 10, i), (7, 8, j), (8, 8, k(0 − 3)), (8, 9, l), (9, 7, m(2 − 5)), (9, 10, n)} (a) Draw the graph. (b) What is the maximum number of paths through the graph? (c) What is the approximate minimum number of paths through the graph?
2.8 BIBLIOGRAPHIC NOTES During the research for this book, one thing that became abundantly clear is that this field has had a significant amount of parallel discovery of the same techniques by people working independently. Some individuals have discovered various aspects of the same technique, which was subsequently polished into very pretty test criteria. Others have invented the same techniques, but based them on different types of graphs or used different names. Thus, ascribing credit for software testing criteria is a perilous task. We do our best, but claim only that the bibliographic notes in this book are starting points for further study in the literature. The research into covering graphs seems to have started with generating tests from finite state machines (FSMs), which has a long and rich history. Some of the earliest papers were in the 1970s [77, 164, 170, 232, 290]. The primary focus of most of these papers was on using FSMs to generate tests for telecommunication systems that were defined with standard finite automata, although much of the work pertained to general graphs. The control flow graph seems to have been invented (or should it be termed "discovered"?) by Legard in 1975 [204]. In papers published
in 1975, Huang [170] suggested covering each edge in the FSM, and Howden [164] suggested covering complete trips through the FSM, but without looping. In 1976, McCabe [232] suggested the same idea on control flow graphs as the primary application of his cyclomatic complexity metric. In 1976, Pimont and Rault [290] suggested covering pairs of edges, using the term "switch cover." In 1978, Chow [77] suggested generating a spanning tree from the FSM and then basing test sequences on paths through this tree. In 1991, Fujiwara et al. [130] extended Pimont and Rault's pairs of edges to arbitrary lengths, and used the term "n-switch" to refer to a sequence of edges. He also attributed "1-switch," or switch cover, to Chow and called it the "W-method," an inaccuracy that has been repeated in numerous papers. The idea of covering pairs of edges was rediscovered in the 1990s. The British Computer Society Standard for Software Component Testing called it two-trip [317] and Offutt et al. [272], called it transition-pair. Other test generation methods based on FSMs include tour [251], the distinguished sequence method [137], and unique input-output method [307]. Their objectives are to detect output errors based on state transitions driven by inputs. FSMbased test generation has been used to test a variety of applications including lexical analyzers, real-time process control software, protocols, data processing, and telephony. One early realization when developing this book is that the criteria for covering FSMs are not substantially different from criteria for other graphs. This book has introduced the explicit inclusion of node coverage requirements in edge coverage requirements (the "up to" clause). This inclusion is not necessary for typical control flow graphs, where, indeed, subsumption of node coverage by edge coverage is often presented as a basic theorem, but it may be required for graphs derived from other artifacts. Several later papers focused on automatic test data generation to cover structural elements in the program [39, 41, 80, 101, 117, 166, 190, 191, 267, 295]. Much of this work was based on the analysis techniques of symbolic evaluation [62, 83, 93, 101, 116, 164], and slicing [328, 339]. Some of these ideas are discussed in Chapter 6. The problem of handling loops has plagued graph-based criteria from the beginning. It seems obvious that we want to cover paths, but loops create infinite numbers of paths. In Howden's 1975 paper [164], he specifically addressed loops by covering complete paths "without looping," and Chow's 1978 suggestion to use spanning trees was an explicit attempt to avoid having to execute loops [77]. Binder's book [33] used the technique from Chow's paper, but changed the name to round trip, which is the name used in this book. Another early suggestion was based on testing loop free programs [66], which is certainly interesting from a theoretical view, but not particularly practical. White and Wiszniewski [348] suggested limiting the number of loops that need to be executed based on specific patterns. Weyuker, Weiss, and Hamlet tried to choose specific loops to test based on data definitions and uses [345]. The notion of subpath sets was developed by Offutt et al. [178, 265] to support inter-class path testing and is essentially equivalent to tours with detours as presented here. The ideas of touring, sidetrips and detours were introduced by Ammann, Offutt and Huang [17]. The earliest reference we have found on data flow testing was a technical report in 1974 by Osterweil and Fosdick [282]. This technical report was followed by a 1976
paper in ACM Computing Surveys [122], along with an almost simultaneous publication by Herman in the Australian Computer Journal [158]. The seminal data flow analysis procedure (without reference to testing) was due to Allen and Cocke [13]. Other fundamental and theoretical references are by Laski and Korel in 1983 [201], who suggested executing paths from definitions to uses, Rapps and Weyuker in 1985 [297], who defined criteria and introduced terms such as All-Defs and AllUses, and Frankl and Weyuker in 1988 [128]. These papers refined and clarified the idea of data flow testing, and are the basis of the presentation in this text. Stated in the language in this text, [128] requires direct tours for the All-du-Paths Coverage, but allows sidetrips for All-Defs coverage and All-Uses coverage. This text allows sidetrips (or not) for all of the data flow criteria. The pattern matching example used in this text has been employed in the literature for decades; as far as we know, Frankl and Weyuker [128] were the first to use the example for illustrating data flow coverage. Forman also suggested a way to detect data flow anomalies without running the program [121]. Some detailed problems with data flow testing have been recurring. These include the application of data flow when paths between definitions and uses cannot be executed [127], and handling pointers and arrays [267, 345]. The method of defining data flow criteria in terms of sets of du-paths is original to this book, as is the explicit suggestion for best-effort eouring. Many papers present empirical studies of various aspects of data flow testing. One of the earliest was by Clarke, Podgurski, Richardson, and Zeil, who compared some of the different criteria [82]. Comparisons with mutation testing (introduced in Chapter 5) started with Mathur in 1991 [228], which was followed by Mathur and Wong [230], Wong and Mathur [357], Offutt, Pan, Tewary, and Zhang [274], and Frankl, Weiss, and Hu [125]. Comparisons of data flow with other test criteria have been published by Frankl and Weiss [124], Hutchins, Foster, Goradia, and Ostrand [172], and Frankl and Deng [123]. A number of tools have also been built by researchers to support data flow testing. Most worked by taking a program and tests as inputs, and deciding whether one or more data flow criteria have been satisfied (a recognizer). Frankl, Weiss, and Weyuker built ASSET in the mid 1980s [126], Girgis and Woodward built a tool to implement both data flow and mutation testing in the mid 1980s [134], and Laski built STAD in the late 1980s [200]. Researchers at Bellcore developed the ATAC data flow tool for C programs in the early 1990s [161, 162], and the first tool that included a test data generator for data flow criteria was built by Offutt, Jin, and Pan in the late 1990s [267]. Coupling was first discussed as a design metric by Constantine and Yourdon [88], and its use for testing was introduced implicitly by Harrold, Soffa, and Rothermel [152, 154] and explicitly by Jin and Offutt [178], who introduced the use of first-uses and last-defs. Kim, Hong, Cho, Bae, and Cha used a graph-based approach to generate tests from UML state diagrams [186]. The USA's Federal Aviation Authority (FAA) has recognized the increased importance of modularity and integration testing by imposing requirements on structural coverage analysis of software that "the analysis should confirm the data
coupling and control coupling between the code components" [305], p. 33, section 6.4.4.2. Data flow testing has also been applied to integration testing by Harrold and Soffa [154], Harrold and Rothermel [152], and Jin and Offutt [178]. This work focused on class-level integration issues, but did not address inheritance or polymorphism. Data flow testing has been applied to inheritance and polymorphism in object-oriented software by Alexander and Offutt [11, 10, 12], and Buy, Orso, and Pezze [60, 281]. Gallagher and Offutt modeled classes as interacting state machines, and tested concurrency and communication issues among them [132]. SCR was first discussed by Henninger [157], and its use in model checking and testing was introduced by Atlee [20]. Constructing tests from UML diagrams is a more recent development, though relatively straightforward. It was first suggested by Abdurazik and Offutt [2, 264], and soon followed by Briand and Labiche [45]. The mechanisms for turning finite automata into regular expressions are standard fare in CS theory classes. As far as we know, Beizer [29] was the first to note the utility of these transformations in the testing context.
NOTES 1 By way of example, typical control flow graphs have very few, if any, syntactically unreachable nodes, but call graphs, especially for object-oriented programs, often do. 2 Our mathematician readers might notice that this definition is constructive in that it defines what is in the set T R, but does not actually bound the set. It is certainly our intention that T R contains no other elements. 3 The reader might wonder why NOTFOUND fails to appear in the set use(2). The reason, as explained in Section 2.3.2 is that the use is local. 4 The reader is cautioned that despite the names of the criteria, All-Defs and All-Uses are not complementary criteria with respect to how they tread definitions and uses. Specifically, one does not arrive at All-Uses by replacing the notion of "def" with that of "use" in All-Defs. The reader might find it helpful to note that while All-Defs focuses on definitions, All-Uses focuses on def-use pairs. While one could argue that the naming convention is misleading, and that a name such as "All-Pairs" might be preferable to All-Uses, the authors elected to stick with the standard usage in the dataflow literature. 5 This is a bit of an overstatement, and, as usual, the culprit is infeasibility. Specifically, consider a du-path with respect to variable x that can only be toured with a sidetrip. Further, suppose that there are two possible sidetrips, one of which is def-clear with respect to x, and one of which is not. The relevant test path from the All-du-Paths test set necessarily tours the former sidetrip, where as the corresponding test path from the prime path test set is free to tour the latter side trip. Our opinion is that in most situations it is reasonable for the test engineer to ignore this special case and simply proceed with prime path coverage. 6 As in previous chapters, we explicitly leave out concurrency, so concurrent forks and joins are not considered. 7 Mathematicians who have studied abstract algebra will recognize that these tables define another algebra.
3 Logic Coverage
This chapter introduces test criteria based on logical expressions. While logic coverage criteria have been known for a long time, their use has been steadily growing in recent years. One cause for their use in practice has been their incorporation in standards such as those accepted by the US Federal Aviation Administration (FAA) for safety critical avionics software in commercial aircraft. As in Chapter 2, we start with a sound theoretical foundation for logic predicates and clauses with the goal of making the subsequent testing criteria simpler. As before, we take a generic view of the structures and criteria, then discuss how logic expressions can be derived from various software artifacts, including code, specifications, and finite state machines. Readers who are already familiar with some of the common criteria may have difficulty recognizing them at first. This is because we introduce a generic collection of test criteria, and thus choose names that best help articulate all of the criteria. That is, we are abstracting a number of existing criteria that are closely related, yet use conflicting terminology.
3.1 OVERVIEW: LOGIC PREDICATES AND CLAUSES We formalize logical expressions in a common mathematical way. A predicate is an expression that evaluates to a boolean value, and is our topmost structure. A simple example is: ((a > b) ∨ C) ∧ p(x). Predicates may contain boolean variables, nonboolean variables that are compared with the comparator operators {>, b) || C) && (x < y) o.m(); else o.n();
will yield the expression ((a > b) ∨ C) ∧ (x < y). Other sources of logical expressions include transitions in finite state machines. A transition such as: button2 = true (when gear = park) will yield the expression gear = park ∧ button2 = true. Similarly, a precondition in a specification such as "pre: stack Not full AND object reference parameter not null" will result in a logical expression such as ¬ stackFull() ∧ newObj = null. In the material prior to Section 3.6 we treat logical expressions according to their semantic meanings, not their syntax. As a consequence, a given logical expression yields the same test requirements for a given coverage criterion no matter which form of the logic expression is used.
EXERCISES Section 3.1. 1. List all the clauses for the predicate below: (( f 0)) ∨ (M ∧ (e < d + c)) 2. Write the predicate (only the predicate) to represent the requirement: "List all the wireless mice that either retail for more than $100 or for which the store has more than 20 items. Also list non-wireless mice that retail for more than $50."
3.2 LOGIC EXPRESSION COVERAGE CRITERIA Clauses and predicates are used to introduce a variety of coverage criteria. Let P be a set of predicates and C be a set of clauses in the predicates in P. For each is, C p = {c|c ∈ p}. C is the union of predicate p ∈ P, let C p be the clauses in p, that C p. the clauses in each predicate in P, that is, C = p∈P
Criterion 3.12 Predicate Coverage (PC): For each p ∈ P, T R contains two requirements: p evaluates to true, and p evaluates to false. The graph version of predicate coverage was introduced in Chapter 2 as edge coverage; this is where the graph coverage criteria overlap the logic expression coverage criteria. For control flow graphs where P is the set of predicates associated with branches, predicate coverage and edge coverage are the same. For the predicate given above, ((a > b) ∨ C) ∧ p(x), two tests that satisfy predicate coverage are (a = 5, b = 4, C = true, p(x) = true) and (a = 5, b = 6, C = false, p(x) = false). An obvious failing of this criterion is that the individual clauses are not always exercised. Predicate coverage for the above clause could also be satisfied with the two tests (a = 5, b = 4, C = true, p(x) = true) and (a = 5, b = 4, C = true, p(x) = false), in which the first two clauses never have the value false! To rectify this problem, we move to the clause level. Criterion 3.13 Clause Coverage (CC): For each c ∈ C, T R contains two requirements: c evaluates to true, and c evaluates to false. Our predicate ((a > b) ∨ C) ∧ p(x) requires different values to satisfy CC. Clause coverage requires that (a > b) = true and false, C = true and false, and p(x) = true and false. These requirements can be satisfied with two tests: ((a = 5, b = 4), (C = true), p(x) = true) and ((a = 5, b = 6), (C = false), p(x) = false). Clause coverage does not subsume predicate coverage, and predicate coverage does not subsume clause coverage, as we show with the predicate p = a ∨ b. The clauses C are {a, b}. The four test inputs that enumerate the combinations of logical values for the clauses: 1 2 3 4
a T T F F
b T F T F
a∨b T T T F
Consider two test sets, each with a pair of test inputs. Test set T23 = {2, 3} satisfies clause coverage, but not predicate coverage, because p is never false. Conversely, test set T24 = {2, 4} satisfies predicate coverage, but not clause coverage, because b is never true. These two test sets demonstrate that neither predicate coverage nor clause coverage subsumes the other. From the testing perspective, we would certainly like a coverage criterion that tests individual clauses and that also tests the predicate. The most direct approach to rectify this problem is to try all combinations of clauses:
Logic Coverage
Criterion 3.14 Combinatorial Coverage (CoC): For each p ∈ P, T R has test requirements for the clauses in C p to evaluate to each possible combination of truth values. Combinatorial coverage has also been called multiple condition coverage. For the predicate (a ∨ b) ∧ c, the complete truth table contains eight elements:
a T T T T F F F F
b T T F F T T F F
c T F T F T F T F
(a ∨ b) ∧ c T F T F T F F F
A predicate p with n independent clauses has 2n possible assignments of truth values. Thus combinatorial coverage is unwieldy at best, and impractical for predicates with more than a few clauses. What we need are criteria that capture the effect of each clause, but do so in a reasonable number of tests. These observations lead, after some thought,1 to a powerful collection of test criteria that are based on the notion of making individual clauses "active" as defined in the next subsection. Specifically, we check to see that if we vary a clause in a situation where the clause should affect the predicate, then, in fact, the clause does affect the predicate. Later we turn to the complementary problem of checking to see that if we vary a clause in a situation where it should not affect the predicate, then it, in fact, does not affect the predicate.
3.2.1 Active Clause Coverage The lack of subsumption between clause and predicate coverage is unfortunate, but clause and predicate coverage have deeper problems. Specifically, when we introduce tests at the clause level, we want also to have an effect on the predicate. The key notion is that of determination, the conditions under which a clause influences the outcome of a predicate. Although the formal definition is a bit messy, the basic idea is very simple: if you flip the clause, and the predicate changes value, then the clause determines the predicate. To distinguish the clause in which we are interested from the remaining clauses, we adopt the following convention. The major clause, ci , is the clause on which we are focusing. All of the other clauses c j , j = i, are minor clauses. Typically, to satisfy a given criterion, each clause is treated in turn as a major clause. Formally, Definition 3.42 Determination: Given a major clause ci in predicate p, we say that ci determines p if the minor clauses c j ∈ p, j = i have values so that changing the truth value of ci changes the truth value of p.
Note that this definition explicitly does not require that ci = p. This issue has been left ambiguous by previous definitions, some of which require the predicate and the major clause to have the same value. This interpretation is not practical. When the negation operator is used, for example, if the predicate is p = ¬a, it becomes impossible for the major clause and the predicate to have the same value. Consider the example above, where p = a ∨ b. If b is false, then clause a determines p, because then the value of p is exactly the value of a. However if b is true, then a does not determine p, since p is true regardless of the value of a. From the testing perspective, we would like to test each clause under circumstances where the clause determines the predicate. Consider this as putting different members of a team in charge of the team. We do not know if they can be effective leaders until they try. Consider again the predicate p = a ∨ b. If we do not vary b under circumstances where b determines p, then we have no evidence that b is used correctly. For example, test set T14 = {TT, F F}, which satisfies both clause and predicate coverage, tests neither a nor b effectively. In terms of criteria, we develop the notion of active clause coverage in a general way first with the definition below and then refine out the ambiguities in the definition to arrive at the resulting formal coverage criteria. Definition 3.43 Active Clause Coverage (ACC): For each p ∈ P and each major clause ci ∈ C p , choose minor clauses c j , j = i so that ci determines p. T R has two requirements for each ci : ci evaluates to true and ci evaluates to false. For example, for p = a ∨ b, we end up with a total of four requirements in T R, two for clause a and two for clause b. For clause a, a determines p if and only if b is false. So we have the two test requirements {(a = true, b = false), (a = false, b = false)}. For clause b, b determines p if and only if a is false. So we have the two test requirements {(a = false, b = true), (a = false, b = false)}. This is summarized in the partial truth table below (the values for the major clauses are in bold face). ci = a ci = b
a T F f f
b f f T F
Two of these requirements are identical, so we end up with three distinct test requirements for active clause coverage for the predicate a ∨ b, namely, {(a = true, b = false), (a = false, b = true), (a = false, b = false)}. Such overlap always happens, and it turns out that for a predicate with n clauses, n + 1 distinct test requirements, rather than the 2n one might expect, are sufficient to satisfy active clause coverage. ACC is almost identical to the way early papers described another technique called MCDC. It turns out that this criterion has some ambiguity, which has led to a fair amount of confusion about how to interpret MCDC over the years. The most important question is whether the minor clauses c j need to have the same values when the major clause ci is true as when ci is false. Resolving this ambiguity leads to three distinct and interesting flavors of ACC. For a simple predicate such as p = a ∨ b, the three flavors turn out to be identical, but differences appear for
more complex predicates. The most general flavor allows the minor clauses to have different values. Criterion 3.15 General Active Clause Coverage (GACC): For each p ∈ P and each major clause ci ∈ C p , choose minor clauses c j , j = i so that ci determines p. T R has two requirements for each ci : ci evaluates to true and ci evaluates to false. The values chosen for the minor clauses c j do not need to be the same when ci is true as when ci is false. Unfortunately, it turns out that GACC does not subsume predicate coverage, as the following example shows. Consider the predicate p = a ↔ b. Clause a determines p for any assignment of truth values to b. So, when a is true, we choose b to be true as well, and when a is false, we choose b to be false as well. We make the same selections for clause b. We end up with only two test inputs: {TT, F F}. p evaluates to true for both of these cases, so predicate coverage is not achieved. Many testing researchers have a strong feeling that ACC should subsume PC, thus the second flavor of ACC requires that p evaluates to true for one assignment of values to the major clause ci , and false for the other. Note that ci and p do not have to have the same values, as discussed with the definition for determination. Criterion 3.16 Correlated Active Clause Coverage (CACC): For each p ∈ P and each major clause ci ∈ C p , choose minor clauses c j , j = i so that ci determines p. T R has two requirements for each ci : ci evaluates to true and ci evaluates to false. The values chosen for the minor clauses c j must cause p to be true for one value of the major clause ci and false for the other. So for the predicate p = a ↔ b above, CACC can be satisfied with respect to clause a with the test set {TT, F T} and with respect to clause b with the test set {TT, T F}. Merging these yields the CACC test set {TT, T F, F T}. Consider the example p = a ∧ (b ∨ c). For a to determine the value of p, the expression b ∨ c must be true. This can be achieved in three ways: b true and c false, b false and c true, and both b and c true. So, it would be possible to satisfy CACC with respect to clause a with the two test inputs: {TT F, F F T}. Other choices are possible with respect to a. The following truth table helps enumerate them. The row numbers are taken from the complete truth table for the predicate given previously. Specifically, CACC can be satisfied for a by choosing one test requirement from rows 1, 2, and 3, and the second from rows 5, 6, and 7. Of course, nine possible ways exist to do this. 1 2 3 5 6 7
a T T T F F F
b T T F T T F
c T F T T F T
a ∧ (b ∨ c) T T T F F F
The final flavor forces the c j to be identical for both assignments of truth values to ci . Criterion 3.17 Restricted Active Clause Coverage (RACC): For each p ∈ P and each major clause ci ∈ C p , choose minor clauses c j , j = i so that ci determines p. T R has two requirements for each ci : ci evaluates to true and ci evaluates to false. The values chosen for the minor clauses c j must be the same when ci is true as when ci is false. For the example p = a ∧ (b ∨ c), only three of the nine sets of test requirements that satisfy CACC with respect to clause a will satisfy RACC with respect to clause a. In terms of the previously given complete truth table, row 2 can be paired with row 6, row 3 with row 7, or row 1 with row 5. Thus, instead of the nine ways to satisfy CACC, only three can satisfy RACC.
a T F T F T F
b T T T T F F
c T T F F T T
a ∧ (b ∨ c) T F T F T F
CACC versus RACC Examples of satisfying a predicate for each of these three criteria are given later. One point that may not be immediately obvious is how CACC and RACC differ in practice. It turns out that some logical expressions can be completely satisfied under CACC, but have infeasible test requirements under RACC. These expressions are a little subtle and only exist if dependency relationships exist among the clauses, that is, some combinations of values for the clauses are prohibited. Since this often happens in real programs, because program variables frequently depend upon one another, it is useful to consider such an example. Consider a system with a valve that might be either open or closed, and several modes, two of which are "Operational" and "Standby." Assume the following two constraints: 1. The valve must be open in "Operational" and closed in all other modes. 2. The mode cannot be both "Operational" and "Standby" at the same time. This leads to the following clause definitions: a = "The valve is closed" b = "The system status is Operational" c = "The system status is Standby"
Suppose that a certain action can be taken only if the valve is closed and the system status is either in Operational or Standby. That is, p = valve is closed AND (system status is Operational OR system status is Standby) = a ∧ (b ∨ c) This is exactly the predicate that was analyzed above. The constraints above can be formalized as 1 ¬a ↔ b 2 ¬(b ∧ c) These constraints limit the feasible values in the truth table. As a reminder, the complete truth table for this predicate is
a ∧ (b ∨ c)) T T T F F F F F
violates constraints 1 & 2 violates constraint 1
violates constraint 2 violates constraint 1 violates constraint 1
Recall that for a to determine the value of P, either b or c or both must be true. Constraint 1 rules out the rows where a and b have the same values, that is, rows 1, 2, 7, and 8. Constraint 2 rules out the rows where b and c are both true, that is, rows 1 and 5. Thus, the only feasible rows are 3, 4, and 6. Recall that CACC can be satisfied by choosing one from rows 1, 2, or 3 and one from rows 5, 6, or 7. But RACC requires one of the pairs 2 and 6, 3, and 7, or 1 and 5. Thus, RACC is infeasible for a in this predicate.
3.2.2 Inactive Clause Coverage The Active Clause Coverage criteria focus on making sure the major clauses do affect their predicates. A complementary criterion to ACC ensures that changing a major clause that should not affect the predicate does not, in fact, affect the predicate. Definition 3.44 Inactive Clause Coverage (ICC): For each p ∈ P and each major clause ci ∈ C p , choose minor clauses c j , j = i so that ci does not determine p. T R has four requirements for ci under these circumstances: (1) ci evaluates to true with p true, (2) ci evaluates to false with p true, (3) ci evaluates to true with p false, and (4) ci evaluates to false with p false. Although inactive clause coverage (ICC) has some of the same ambiguity as ACC does, only two distinct flavors can be defined, namely general inactive clause
coverage (GICC) and restricted inactive clause coverage (RICC). The notion of correlation is not relevant for Inactive Clause Coverage because ci cannot correlate with p since ci does not determine p. Also, predicate coverage is guaranteed, subject to feasibility, in all flavors due to the structure of the definition. The following example illustrates the value of the inactive clause coverage criteria. Suppose you are testing the control software for a shutdown system in a reactor, and the specification states that the status of a particular valve (open vs. closed) is relevant to the reset operation in Normal mode, but not in Override mode. That is, the reset should perform identically in Override mode when the valve is open and when the valve is closed. The sceptical test engineer will want to test reset in Override mode for both positions of the valve, since a reasonable implementation mistake would be to take account the setting of the valve in all modes. The formal versions of GICC and RICC are as follows. Criterion 3.18 General Inactive Clause Coverage (GICC): For each p ∈ P and each major clause ci ∈ C p , choose minor clauses c j , j = i so that ci does not determine p. T R has four requirements for ci under these circumstances: (1) ci evaluates to true with p true, (2) ci evaluates to false with p true, (3) ci evaluates to true with p false, and (4) ci evaluates to false with p false. The values chosen for the minor clauses c j may vary amongst the four cases. Criterion 3.19 Restricted Inactive Clause Coverage (RICC): For each p ∈ P and each major clause ci ∈ C p , choose minor clauses c j , j = i so that ci does not determine p. T R has four requirements for ci under these circumstances: (1) ci evaluates to true with p true, (2) ci evaluates to false with p true, (3) ci evaluates to true with p false, and (4) ci evaluates to false with p false. The values chosen for the minor clauses c j must be the same in cases (1) and (2), and the values chosen for the minor clauses c j must also be the same in cases (3) and (4).
3.2.3 Infeasibility and Subsumption A variety of technical issues complicate the Active Clause Coverage criteria. As with many criteria, the most important is the issue of infeasibility. Infeasibility is often a problem because clauses are sometimes related to one another. That is, choosing the truth value for one clause may affect the truth value for another clause. Consider, for example, a common loop structure, which assumes short circuit semantics: while (i < n && a[i] != 0) {do something to a[i]}
The idea here is to avoid evaluating a[i] if i is out of range, and short circuit evaluation is not only assumed, but depended on. Clearly, it is not going to be possible to develop a test case where i < n is false and a[i] != 0 is true. In principle, the issue of infeasibility for clause and predicate criteria is no different from that for graph criteria. In both cases, the solution is to satisfy test requirements that are feasible, and then decide how to treat infeasible test requirements.
Complete Clause Coverage CoC
Restricted Active Clause Coverage RACC
Correlated Active Clause Coverage CACC
Restricted Inactive Clause Coverage RICC
General Inactive Clause Coverage GICC
General Active Clause Coverage GACC
Clause Coverage CC
Predicate Coverage PC
Figure 3.1. Subsumption relations among logic coverage criteria.
The simplest solution is to simply ignore infeasible requirements, which usually does not affect the quality of the tests. However, a better solution for some infeasible test requirements is to consider the counterparts of the requirements in a subsumed coverage criterion. For example, if RACC coverage with respect to clause a in predicate p is infeasible (due to additional constraints between the clauses), but CACC coverage is feasible, then it makes sense to replace the infeasible RACC test requirements with the feasible CACC test requirements. This approach is similar to that of best-effort touring developed in the graph coverage chapter. Figure 3.1 shows the subsumption relationships among the logic expression criteria. Note that the ICC criteria do not subsume any of the ACC criteria, and vice versa. The diagram assumes that infeasible test requirements are treated on a best effort basis, as explained above. Where such an approach does not result in feasible test requirements, the diagram assumes that the infeasible test requirements are ignored.
3.2.4 Making a Clause Determine a Predicate So, how does one go about finding values for the minor clauses c j so that the major clause ci determines the value of p? The authors are aware of three different methods presented in the literature; we give a direct definitional approach here. Pointers
to the other two, one of which is an algorithmic version of the definitional approach, are given in the bibliographic notes. For a predicate p with clause (or boolean variable) c, let pc=true represent the predicate p with every occurrence of c replaced by true and pc=false be the predicate p with every occurrence of c replaced by false. For the rest of this development, we assume no duplicates (that is, p contains only one occurrence of c). Note that neither pc=true nor pc=false contains any occurrences of the clause c. Now we connect the two expressions with an exclusive or: pc = pc=true ⊕ pc=false It turns out that pc describes the exact conditions under which the value of c determines that of p. That is, if values for the clauses in pc are chosen so that pc is true, then the truth value of c determines the truth value of p. If the clauses in pc are chosen so that pc evaluates to false, then the truth value of p is independent of the truth value of c. This is exactly what we need to implement the various flavors of active and inactive clause coverage. As a first example, we try p = a ∨ b. pa is, by definition, pa = = = =
pa=true ⊕ pa=false (true ∨ b) ⊕ (false ∨ b) true ⊕ b ¬b
That is, for the major clause a to determine the predicate p, the only minor clause b must be false. This should make sense intuitively, since the value of a will have an effect on the value of p only if b is false. By symmetry, it is clear that pb is ¬a. If we change the predicate to p = a ∧ b, we get pa = = = =
pa=true ⊕ pa=false (true ∧ b) ⊕ (false ∧ b) b ⊕ false b
That is, we need b = true to make a determine p. By a similar analysis, pb = a. The equivalence operator is a little less obvious and brings up an interesting point. Consider p = a ↔ b. pa = = = =
pa=true ⊕ pa=false (true ↔ b) ⊕ (false ↔ b) b ⊕ ¬b true
That is, for any value of b, a determines the value of p without regard to the value for b! This means that for a predicate p, such as this one, where the value of pc is the constant true, the ICC criteria are infeasible with respect to c. Inactive clause coverage is likely to result in infeasible test requirements when applied to expressions that use the equivalence or exclusive-or operators. A more general version of this conclusion can be drawn that applies to the ACC criteria as well. If a predicate p contains a clause c such that pc evaluates to the constant false, the ACC criteria are infeasible with respect to c. The ultimate reason
is that the clause in question is redundant; the predicate can be rewritten without it. While this may sound like a theoretical curiosity, it is actually a very useful result for testers. If a predicate contains a redundant clause, this is a very strong signal that something is wrong with the predicate! Consider p = a ∧ b ∨ a ∧ ¬b. This is really just the predicate p = a; b is irrelevant. Computing pb, we get pb = pb=true ⊕ pb=false = (a ∧ true ∨ a ∧ ¬true) ⊕ (a ∧ false ∨ a ∧ ¬false) = (a ∨ false) ⊕ (false ∨ a) = a⊕a = false so it is impossible for b to determine p. We need to consider how to make clauses determine predicates for a couple of more complicated expressions. For the expression p = a ∧ (b ∨ c), we get pa = pa=true ⊕ pa=false = (true ∧ (b ∨ c)) ⊕ (false ∧ (b ∨ c)) = (b ∨ c) ⊕ false = b ∨ c. This example ends with an undetermined answer, which points out the key difference between CACC and RACC. Three choices of values make b ∨ c true, (b = c = true), (b = true, c = false), and (b = false, c = true). For CACC, we could pick one pair of values when a is true and another when a is false. For RACC, we must choose the same pair for both values of a. The derivation for b and equivalently for c is slightly more complicated: pb = pb=true ⊕ pb=false = (a ∧ (true ∨ c)) ⊕ (a ∧ (false ∨ c)) = (a ∧ true) ⊕ (a ∧ c) = a ⊕ (a ∧ c) = a ∧ ¬c The last step in the simplification shown above may not be immediately obvious. If it is not, try constructing the truth table for a ⊕ (a ∧ c). The computation for pc is equivalent and yields the solution a ∧ ¬b.
3.2.5 Finding Satisfying Values The final step in applying the logic coverage criteria is to choose values that satisfy the criteria. This section shows how to generate values for one example; more cases are explored in the exercises and the application sections later in the chapter. The example is from the first section of the chapter: p = (a ∨ b) ∧ c Finding values for predicate coverage is easy and was already shown in Section 3.2. Two test requirements are T RPC = { p = true, p = false}
and they can be satisfied with the following values for the clauses: b t t
a t t
p = true p = false
c t f
To run the test cases, we need to refine these truth assignments to create values for clauses a, b, and c. Suppose that clauses a, b, and c were defined in terms of Java program variables as follows: a b c
x < y, a relational expression for program variables x and y done, a primitive boolean value list.contains(str), for List and String objects
Thus, the complete expanded predicate is actually p = (x < y ∨ done) ∧ list.contains(str) Then the following values for the program variables satisfy the test requirements for predicate coverage. b done = true done = true
a p = true p = false
x=3 x=0
y=5 y=7
c list=["Rat," "Cat," "Dog"] str = "Cat" list=["Red," "White"] str = "Blue"
Note that the values for the program variables need not be the same in a particular test case if the goal is to set a clause to a particular value. For example, clause a is true in both tests, even though program variables x and y have different values. Values to satisfy clause coverage were also shown in Section 3.2. Six test requirements are T RCC = {a = true, a = false, b = true, b = false, c = true, c = false} and they can be satisfied with the following values for the clauses (blank cells represent "don't-care" values):
a = true a = false b = true b = false c = true c = false
a t f
t f t f
Refining the truth assignments to create values for program variables x, y, done, list, and str is left as an exercise for the reader. Before proceeding with the other criteria, we first choose values for minor clauses to ensure that the major clauses will determine the value of p. We gave a
method of calculating pa , pb, and pc earlier. The computations for this particular predicate p are left as an exercise. However, the results are ¬b ∧ c ¬a ∧ c a∨b
pa pb pc
Now we can turn to the other clause coverage criteria. The first is combinatorial coverage, requiring all combinations of values for the clauses. In this case, we have eight test requirements, which can be satisfied with the following values:
Recall that general active clause coverage requires that each major clause be true and false and the minor clauses be such that the major clause determines the value of the predicate. Similarly to clause coverage, three pairs of test requirements can be defined: T RGACC = {(a = true ∧ pa , a = false ∧ pa ), (b = true ∧ pb, b = false ∧ pb), (c = true ∧ pc , c = false ∧ pc )} The test requirements can be satisfied with the following values for the clauses. Note that these can be the same as with clause coverage with the exception that the blank cells from clause coverage are replaced with the values from the determination analysis. In the following (partial truth) table, values for major clauses are indicated with upper case letters in boldface. a = true ∧ pa a = false ∧ pa b = true ∧ pb b = false ∧ pb c = true ∧ pc c = false ∧ pc
a T F f f t f
b f f T F f t
c t t t t T F
p t f t f t f
Note the duplication; the first and fifth rows are identical, and the second and fourth are identical. Thus, only four tests are needed to satisfy GACC. A different way of looking at GACC considers all of the possible pairs of test inputs for each pair of test requirements. Recall that the active clause coverage criteria always generate test requirements in pairs, with one pair generated for each clause in the predicate under test. To identify these test inputs, we will use the row
numbers from the truth table. Hence, the pair (3, 7) represents the first two tests listed in the table above. It turns out that (3, 7) is the only pair that satisfies the GACC test requirements with respect to clause a (when a is major), and (5, 7) is the only pair that satisfies the GACC test requirements with respect to clause b. For clause c, the situation is more interesting. Nine pairs satisfy the GACC test requirements for clause c, namely {(1, 2), (1, 4), (1, 6), (3, 2), (3, 4), (3, 6), (5, 2), (5, 4), (5, 6)} Recall that correlated active clause coverage requires that each major clause be true and false, the minor clauses be such that the major clause determines the value of the predicate, and the predicate must have both the value true and false. As with GACC, three pairs of test requirements can be defined: For clause a, the pair of test requirements is a = true ∧ pa ∧ p = x a = false ∧ pa ∧ p = ¬x where x may be either true or false. The point is that p must have a different truth value in the two test cases. We leave the reader to write out the corresponding CACC test requirements with respect to b and c. For our example predicate p, a careful examination of the pairs of test cases for GACC reveals that p takes on both truth values in each pair. Hence, GACC and CACC are the same for predicate p, and the same pairs of test inputs apply. In the exercises the reader will find predicates where a test pair that satisfies GACC with respect to some clause c turns out not to satisfy CACC with respect to c. The situation for RACC is quite different, however, in the example p. Recall that restricted active clause coverage is the same as CACC except that it requires the values for the minor clauses c j to be identical for both assignments of truth values to the major clause, ci . For clause a, the pair of test requirements that RACC generates is a = true ∧ pa ∧ b = B ∧ c = C a = false ∧ pa ∧ b = B ∧ c = C for some boolean constants B and C. An examination of the pairs given above for GACC reveals that with respect to clauses a and b, the pairs are the same. So pair (3, 7) satisfies RACC with respect to clause a and pair (5, 7) satisfies RACC with respect to b. However, with respect to c, only three of the pairs satisfy RACC, namely, {(1, 2), (3, 4), (5, 6)} This example does leave one question about the different flavors of the ACC criteria, namely, what is the practical difference among them? That is, beyond the subtle difference in the arithmetic, how do they affect practical testers? The real differences do not show up very often, but when they do they can be dramatic and quite annoying. GACC does not require that predicate coverage be satisfied on the pair of tests for each clause, so use of that flavor may mean we do not test our program as thoroughly as we might like. In practical use, it is easy to construct examples where
GACC is satisfied but predicate coverage is not, when the predicates are very small (one or two terms), but difficult with three or more terms, since for one of the clauses, it is likely that the chosen GACC tests will also be CACC tests. The restrictive nature of RACC, on the other hand, can sometimes make it hard to satisfy the criterion. This is particularly true when some combinations of clause values are infeasible. Assume that in the predicate used above, the semantics of the program effectively eliminate rows 2, 3, and 6 from the truth table. Then RACC cannot be satisfied with respect to clause list.contains(str) (that is, we have infeasible test requirements), but CACC can. The wise reader, (that is, if still awake) will by now realize that Correlated Active Clause Coverage is often the most practical flavor of ACC.
EXERCISES Section 3.2. Use predicates (1) through (10) to answer the following questions. 1. 2. 3. 4. 5. 6. 7. 8. 9. 10.
p = a ∧ (¬b ∨ c) p = a ∨ (b ∧ c) p=a∧b p = a → (b → c) p=a⊕b p = a ↔ (b ∧ c) p = (a ∨ b) ∧ (c ∨ d) p = (¬a ∧ ¬b) ∨ (a ∧ ¬c) ∨ (¬a ∧ c) p = a ∨ b ∨ (c ∧ d) p = (a ∧ b) ∨ (b ∧ c) ∨ (a ∧ c) (a) Identify the clauses that go with predicate p. (b) Compute (and simplify) the conditions under which each of the clauses determines predicate p. (c) Write the complete truth table for all clauses. Label your rows starting from 1. Use the format in the example underneath the definition of combinatorial coverage in Section 3.2. That is, row 1 should be all clauses true. You should include columns for the conditions under which each clause determines the predicate, and also a column for the predicate itself. (d) Identify all pairs of rows from your table that satisfy general active clause coverage (GACC) with respect to each clause. (e) Identify all pairs of rows from your table that satisfy correlated active clause coverage (CACC) with respect to each clause. (f) Identify all pairs of rows from your table that satisfy restricted active clause coverage (RACC) with respect to each clause. (g) Identify all 4-tuples of rows from your table that satisfy general inactive clause coverage (GICC) with respect to each clause. Identify any infeasible GICC test requirements. (h) Identify all 4-tuples of rows from your table that satisfy restricted inactive clause coverage (RICC) with respect to each clause. Identify any infeasible RICC test requirements.
11. Refine the GACC, CACC, RACC, GICC, and RICC coverage criteria so that the constraints on the minor clauses are made more formal. 12. (Challenging!) Find a predicate and a set of additional constraints so that CACC is infeasible with respect to some clause, but GACC is feasible.
3.3 STRUCTURAL LOGIC COVERAGE OF PROGRAMS As with graph coverage criteria, the logic coverage criteria apply to programs in a straightforward way. Predicates are derived directly from decision points in the programs (if, case, and loop statements). Although these criteria are difficult to apply when predicates have a large number of clauses, this is often not a problem with programs. The vast majority of predicates in programs have only one clause, and programmers tend to write predicates with a maximum of two or three clauses. It should be clear that when a predicate only has one clause, all of the logic coverage criteria collapse into the same criterion – predicate coverage. The primary complexity of applying logic coverage to programs has more to do with reachability than with the criteria. That is, a logic coverage criterion imposes test requirements that are related to specific decision points (statements) in the program. Getting values that satisfy those requirements is only part of the problem; getting to the statement is sometimes more difficult. Two issues are associated with getting there. The first is simply that of reachability from Chapter 1; the test case must include values to reach the statement. In small programs (that is, most methods) this problem is not hard, but when applied within the context of an entire arbitrarily large program, satisfying reachability can be enormously complex. The values that satisfy reachability are prefix values in the test case. The other part of "getting there" can be even harder. The test requirements are expressed in terms of program variables that may be defined locally to the unit or even the statement block being tested. Our test cases, on the other hand, can include values only for inputs to the program that we are testing. Therefore these internal variables have to be resolved to be in terms of the input variables. Although the values for the variables in the test requirements should ultimately be a function of the values of the input variables, this relationship may be arbitrarily complex. In fact, this internal variable problem is formally undecidable. Consider an internal variable X that is derived from a table lookup, where the index to the table is determined by a complex function whose inputs are program inputs. To choose a particular value for X, the tester has to work backward from the statement where the decision appears, to the table where X was chosen, to the function, and finally to an input that would cause the function to compute the desired value. If the function includes randomness or is time sensitive, or if the input cannot be controlled by the tester, it may be impossible to satisfy the test requirement with certainty. This controllability problem has been explored in depth in the automatic test data generation literature and will not be discussed in detail here, except to note that this problem is a major reason why the use of program-level logic coverage criteria is usually limited to unit and module testing activities. The example program in Figures 3.2 and 3.3 is used to illustrate logic coverage on programs.2 The program is a simple triangle classification program called TriTyp. This program (or more accurately, the algorithm) has been used as an example in
Logic Coverage 1 2 3 4 5 6 7 8
// Jeff Offutt--Java version Feb 2003 // Classify triangles import java.io.*;
class trityp { private static String[] triTypes = { "", // Ignore 0. "scalene", "isosceles", "equilateral", "not a valid triangle"}; 9 private static String instructions = "This is the ancient TriTyp program.\nEnter three integers that represent the lengths of the sides of a triangle.\nThe triangle will be categorized as either scalene, isosceles, equilateral\n or invalid.\n"; 10 11 public static void main (String[] argv) 12 { // Driver program for trityp 13 int A, B, C; 14 int T; 15 16 System.out.println (instructions); 17 System.out.println ("Enter side 1: "); 18 A = getN(); 19 System.out.println ("Enter side 2: "); 20 B = getN(); 21 System.out.println ("Enter side 3: "); 22 C = getN(); 23 T = Triang (A, B, C); 24 25 System.out.println ("Result is: " + triTypes[T]); 26 } 27 28 // ==================================== 29 // The main triangle classification method 30 private static int Triang (int Side1, int Side2, int Side3) 31 { 32 int triOut; 33 34 // triOut is output from the routine: 35 // Triang = 1 if triangle is scalene 36 // Triang = 2 if triangle is isosceles 37 // Triang = 3 if triangle is equilateral 38 // Triang = 4 if not a triangle 39 40 // After a quick confirmation that it's a valid 41 // triangle, detect any sides of equal length 42 if (Side1 S3) && (S2+S3 > S1) && (S1+S3 > S2) 70: P1 && (triOut != 0) 72: P1 && (triOut != 0) && (triOut S3)
p74:
(triOut == 2 ∧ S1 + S3 > S2)
Clauses T F f f T F f f T F t T F t T F t
f f T f f f T f t t F t t F t t F
f f f T f f f T – – – – – – – – –
0 1 1 1 2 2 6 2 2 2 2 2 2 2 3 3 5
(0, 0, 0) is the only test that has this problem.) Values to satisfy CACC are shown in Table 3.3.
3.3.1 Predicate Transformation Issues ACC criteria are considered to be expensive for testers, and attempts have been made to reduce the cost. One approach is to rewrite the program to eliminate multiclause predicates, thus reducing the problem to branch testing. A conjecture is that the resulting tests will be equivalent to ACC. However, we explicitly advise against this approach for two reasons. One, the resulting rewritten program may have substantially more complicated control structure than the original (including repeated statements), thus endangering both reliability and maintainability. Second, as the following examples demonstrate, the transformed program may not require tests that are equivalent to the tests for ACC on the original program. Consider the following program segment, where a and b are arbitrary boolean clauses and S1 and S2 are arbitrary statements. S1 and S2 could be single statements, block statements, or function calls. if (a && b) S1; else S2;
The CACC criterion requires the test specifications (t, t), (t, f ), and ( f, t) for the predicate a ∧ b. However, if the program segment is transformed into the following functionally equivalent structure:
if (a) { if (b) S1; else S2; } else S2;
the predicate coverage criterion requires three tests: (t, t) to reach statement S1, (t, f ) to reach the first occurrence of statement S2, and either ( f, f ) or ( f, t) to reach the second occurrence of statement S2. Choosing (t, t), (t, f ), and ( f, f ) means that our tests do not satisfy CACC in that they do not allow a to determine fully the predicate's value. Moreover, the duplication of S2 in the above example has been taught to be poor programming for years, because of the potential for mistakes when duplicating code. A larger example reveals the flaw even more clearly. Consider the simple program segment if ((a && b) || c) S1; else S2;
A straightforward rewrite of this program fragment to remove the multiclause predicate results in this complicated ugliness: if (a) if (b) if (c) S1; else S1; else if (c) S1; else S2; else if (b) if (c) S1;
else S2; else if (c) S1; else S2;
This fragment is cumbersome in the extreme, and likely to be error-prone. Applying the predicate coverage criterion to this would be equivalent to applying combinatorial coverage to the original predicate. A reasonably clever programmer (or good optimizing compiler) would simplify it as follows: if (a) if (b) S1; else if (c) S1; else S2; else if (c) S1; else S2;
This fragment is still much harder to understand than the original. Try to imagine a maintenance programmer trying to change this thing! The following table illustrates truth assignments that can be used to satisfy CACC for the original program segment and predicate testing for the modified version. An 'X' under CACC or predicate indicates that truth assignment is used to satisfy the criterion for the appropriate program fragment. Clearly, predicate coverage on an equivalent program is not the same as CACC testing on the original. Predicate coverage on this modified program does not subsume CACC, and CACC does not subsume predicate coverage. 1 2 3 4 5 6 7 8
((a ∧ b) ∨ c) T T T F T F T F
CACC X X X
Predicate X X X X
EXERCISES Section 3.3. 1. Answer the following questions for the method checkIt() below: public static void checkIt (boolean a, boolean b, boolean c) { if (a && (b || c)) { System.out.println ("P is true"); } else { System.out.println ("P isn't true"); } }
Transform checkIt() to checkItExpand(), a method where each if statement tests exactly one boolean variable. Instrument checkItExpand() to record which edges are traversed. ("print" statements are fine for this.) Derive a GACC test set T1 for checkIt(). Derive an edge coverage test set T2 for checkItExpand(). Build T2 so that it does not satisfy GACC on the predicate in checkIt(). Run both T1 and T2 on both checkIt() and checkItExpand(). 2. Answer the following questions for the method twoPred() below: public String twoPred (int x, int y) { boolean z; if (x < y) z = true; else z = false; if (z && x+y == 10) return "A"; else return "B"; }
Identify test inputs for twoPred() that achieve Restricted Active Clause Coverage (RACC).
Identify test inputs for twoPred() that achieve Restricted Inactive Clause Coverage (RICC). 3. Answer the following questions for the program fragments below:
fragment P: if (A || B || C) { m(); } return;
fragment Q: if (A) { m(); return; } if (B) { m(); return; } if (C) { m(); }
Give a GACC test set for fragment P. (Note that GACC, CACC, and RACC yield identical test sets for this example.)
Does the GACC test set for fragment P satisfy edge coverage on fragment Q? Write down an edge coverage test set for fragment Q. Make your test set include as few tests from the GACC test set as possible. 4. (Challenging!) For the TriTyp program, complete the test sets for the following coverage criteria by filling in the "don't care" values, ensuring reachability, and deriving the expected output. Download the program, compile it, and run it with your resulting test cases to verify correct outputs. Predicate coverage (PC) Clause coverage (CC) Combinatorial coverage (CoC) Correlated active clause coverage (CACC) 5. Repeat the prior exercise, but for the TestPat program in Chapter 2. 6. Repeat the prior exercise, but for the Quadratic program in Chapter 2.
3.4 SPECIFICATION-BASED LOGIC COVERAGE Software specifications, both formal and informal, appear in a variety of forms and languages. They almost invariably include logical expressions, allowing the logic coverage criteria to be applied. We start by looking at their application to simple preconditions on methods. Programmers often include preconditions as part of their methods. The preconditions are sometimes written as part of the design, and sometimes added later as documentation. Specification languages typically make preconditions explicit with the goal of analyzing the preconditions in the context of an invariant. A tester may consider developing the preconditions specifically as part of the testing process if preconditions do not exist. For a variety of reasons, including defensive programming and security, transforming preconditions into exceptions is common practice. In brief, preconditions are common and rich sources of predicates in specifications,
Coverage Criteria public static int cal (int month1, int day1, int month2, int day2, int year) { //*********************************************************** // Calculate the number of Days between the two given days in // the same year. // preconditions : day1 and day2 must be in same year // 1 minVal) ∨ (A < minVal)) ∧ (minVal = A) Finally, the first disjunct can be reduced to a simple inequality, resulting in the following contradiction: (A = minVal) ∧ (minVal = A) The contradiction means that no values exist that can satisfy the conditions, thus the mutant is provably equivalent. In general, detecting equivalent mutants, just like detecting infeasible paths, is an undecidable problem. However, strategies
such as algebraic manipulations and program slicing can detect some equivalent mutants. As a final example, consider the following method, with one mutant shown embedded in statement 4: 1 2 3 4 4 5 6 7 8 9
boolean isEven (int X) { if (X < 0) X = 0 - X; X = 0; if (float) (X/2) == ((float) X) / 2.0 return (true); else return (false); }
The reachability condition for mutant 4 is (X < 0) and the infection condition is (X = 0). If the test case X = -6 is given, then the value of X after statement 4 is executed is 6 and the value of X after the mutated version of statement 4 is executed is 0. Thus, this test satisfies reachability and infection, and the mutant will be killed under the weak mutation criterion. However, 6 and 0 are both even, so the decision starting on statement 5 will return true for both the mutated and nonmutated versions. That is, propagation is not satisfied, so test case X = -6 will not kill the mutant under the strong mutation criterion. The propagation condition for this mutant is that the number be odd. Thus, to satisfy the strong mutation criterion, we require (X < 0) ∧ (X = 0) ∧ odd(X), which can be simplified to X is an odd, negative integer.
Testing Programs with Mutation A test process gives a sequence of steps to follow to generate test cases. A single criterion may be used with many processes, and a test process may not even include a criterion. Choosing a test process for mutation is particularly difficult because mutation analysis is actually a way to measure the quality of the test cases and the actual testing of the software is a side effect. In practical terms, however, the software is tested, and tested well, or the test cases do not kill mutants. This point can best be understood by examining a typical mutation analysis process. Figure 5.2 shows how mutation testing can be applied. The tester submits the program under test to an automated system, which starts by creating mutants. Optionally, those mutants are then analyzed by a heuristic that detects and eliminates as many equivalent mutants as possible.2 A set of test cases is then generated automatically and executed first against the original program, and then the mutants. If the output of a mutant program differs from the original (correct) output, the mutant is marked as being dead and is considered to have been strongly killed by that test case. Dead mutants are not executed against subsequent test cases. Test cases that do not strongly kill at least one mutant are considered to be "ineffective" and eliminated, even though such test cases may weakly kill one or more mutants. This
Syntax-Based Testing
Prog P
Input test program
Create mutants
Run equivalence heuristic
Generate test cases
Define threshold No
Threshold reached ?
Fix P
P (T) correct ?
Run T on P
Run T on mutants: schema-based weak selective
Eliminate ineffective TCs
Figure 5.2. Mutation testing process. Bold boxes represent steps that are automated; other boxes represent manual steps.
is because the requirement stated above requires the output (and not the internal state) to be different. Once all test cases have been executed, coverage is computed as a mutation score. The mutation score is the ratio of dead mutants over the total number of non-equivalent mutants. If the mutation score reaches 1.00, that means all mutants have been detected. A test set that kills all the mutants is said to be adequate relative to the mutants. A mutation score of 1.00 is usually impractical, so the tester defines a "threshold" value, which is a minimum acceptable mutation score. If the threshold has not been reached, then the process is repeated, each time generating test cases to target live mutants, until the threshold mutation score is reached. Up to this point, the process has been entirely automatic. To finish testing, the tester will examine expected output of the effective test cases, and fix the program if any faults are found. This leads to the fundamental premise of mutation testing: In practice, if the software contains a fault, there will usually be a set of mutants that can only be killed by a test case that also detects that fault.
Designing Mutation Operators Mutation operators must be chosen for each language and although they overlap quite a bit, some differences are particular to the language, often depending on the language features. Researchers have designed mutation operators for various programming languages, including Fortran IV, COBOL, Fortran 77, C, C integration testing, Lisp, Ada, Java, and Java class relationships. Researchers have also designed Mutation operators for the formal specification language SMV (discussed in Section 5.4.2), and for XML messages (discussed in Section 5.5.2). As a field, we have learned a lot about designing mutation operators over the years. Detailed lists of mutation operators for various languages are provided in
the literature, as referenced in the bibliographic notes for this chapter. Mutation operators are generally designed either to mimic typical programmer mistakes, or to encourage testers to follow common testing heuristics. Operators that change relational operators or variable references are examples of operators that mimic typical programmer mistakes. The failOnZero() operator used in Figure 5.1 is an example of the latter design; the tester is encouraged to follow the common testing heuristic of "causing each expression to become zero." When first designing mutation operators for a new language, it is reasonable to be "inclusive," that is, include as many operators as possible. However, this often results in a large number of mutation operators, and an even larger number of mutants. Researchers have devoted a lot of effort to trying to find ways to use fewer mutants and mutation operators. The two most common ways to have fewer mutants are (1) to randomly sample from the total number of mutants, and (2) to use mutation operators that are particularly effective. The term selective mutation has been used to describe the strategy of using only mutation operators that are particularly effective. Effectiveness has been evaluated as follows: if tests that are created specifically to kill mutants created by mutation operator oi also kill mutants created by mutation operator o j with very high probability, then mutation operator oi is more effective than o j . This notion can be extended to consider a collection of effective mutation operators as follows: Definition 5.51 Effective Mutation Operators: If tests that are created specifically to kill mutants created by a collection of mutation operators O = {o1 , o2 , . . .} also kill mutants created by all remaining mutation operators with very high probability, then O defines an effective set of mutation operators. Researchers have concluded that a collection of mutation operators that insert unary operators and that modify unary and binary operators will be effective. The actual research was done with Fortran 77 (the Mothra system), but the results are adapted to Java in this chapter. Corresponding operators can be defined for other languages. The operators defined below are used throughout the remainder of this chapter as the defining set of program-level mutation operators. 1. ABS – Absolute Value Insertion: Each arithmetic expression (and subexpression) is modified by the functions abs(), negAbs(), and failOnZero(). abs() returns the absolute value of the expression and negAbs() returns the negative of the absolute value. failOnZero() tests whether the value of the expression is zero. If it is, the mutant is killed; otherwise, execution continues and the value of the expression is returned. This operator is designed specifically to force the tester to cause each numeric expression to have the value 0, a negative value, and a positive value. For example, the statement "x = 3 * a;" is mutated to create the following three statements: x = 3 * abs (a); x = 3 * - abs (a); x = 3 * failOnZero (a);
2. AOR – Arithmetic Operator Replacement: Each occurrence of one of the arithmetic operators +, −, ∗, /, ∗∗, and % is replaced by each of the other operators. In addition, each is replaced by the special mutation operators leftOp, rightOp, and mod. leftOp returns the left operand (the right is ignored), rightOp returns the right operand, and mod computes the remainder when the left operand is divided by the right. For example, the statement "x = a + b;" is mutated to create the following seven statements: x = a - b; x = a * b; x = a / b; x = a ** b; x = a; x = b; x = a % b; 3. ROR – Relational Operator Replacement: Each occurrence of one of the relational operators (, ≥, =, =) is replaced by each of the other operators and by falseOp and trueOp. falseOp always returns false and trueOp always returns true. For example, the statement "if (m > n)" is mutated to create the following seven statements: if (m >= n) if (m < n) if (m >> is replaced by each of the other operators. In addition, each is replaced by the special mutation operator leftOp. leftOp returns the left operand unshifted. For example, the statement "x = m > a; x = m >>> a; x = m; 6. LOR – Logical Operator Replacement: Each occurrence of each bitwise logical operator (bitwise and (&), bitwise or (|), and exclusive or (ˆ)) is replaced by each of the other operators; in addition, each is replaced by leftOp and rightOp. leftOp returns the left operand (the right is ignored) and rightOp returns the right operand. For example, the statement "x = m & n;" is mutated to create the following four statements: x = m | n; x = m ˆ n; x = m; x = n; 7. ASR – Assignment Operator Replacement: Each occurrence of one of the assignment operators (+=, -=, *=, /=, %=, &=, |=, ˆ=, =, >>>=) is replaced by each of the other operators. For example, the statement "x += 3;" is mutated to create the following ten statements: x -= 3; x *= 3; x /= 3; x %= 3; x &= 3; x |= 3; x ˆ= 3; x = 3; x >>>= 3;
8. UOI – Unary Operator Insertion: Each unary operator (arithmetic +, arithmetic −, conditional !, logical ∼) is inserted before each expression of the correct type. For example, the statement "x = 3 * a;" is mutated to create the following four statements: x = 3 * +a; x = 3 * -a; x = +3 * a; x = -3 * a; 9. UOD – Unary Operator Deletion: Each unary operator (arithmetic +, arithmetic −, conditional !, logical ∼) is deleted. For example, the statement "if !(a > -b)" is mutated to create the following two statements: if (a > -b) if !(a > b) Two other operators that are useful in examples are scalar variable replacement and the "bomb" operator. Scalar variable replacement results in a lot of mutants (V 2 if V is the number of variables), and it turns out that it is not necessary given the above operators. It is included here as a convenience for examples. The bomb operator results in only one mutant per statement, but it is also not necessary given the above operators. 10. SVR – Scalar Variable Replacement: Each variable reference is replaced by every other variable of the appropriate type that is declared in the current scope. For example, the statement "x = a * b;" is mutated to create the following six statements: x = a * a; a = a * b; x = x * b; x = a * x; x = b * b; b = a * b; 11. BSR—Bomb Statement Replacement: Each statement is replaced by a special Bomb() function. Bomb() signals a failure as soon as it is executed, thus requiring the tester to reach each statement. For example, the statement "x = a * b;" is mutated to create the following statement: Bomb();
Subsumption of Other Test Criteria (Advanced Topic) Mutation is widely considered the strongest test criterion in terms of finding the most faults. It is also the most expensive. This section shows that mutation subsumes a number of other coverage criteria. The proofs are developed by showing that specific mutation operators impose requirements that are identical to a specific coverage criterion. For each specific requirement defined by a criterion, a single mutant is created that can be killed only by test cases that satisfy the requirement. Therefore, the coverage criterion is satisfied if and only if the mutants associated with the requirements for the criterion are killed. In this case, the mutation operators that ensure coverage of a criterion are said to yield the criterion. If a criterion is yielded by one or more mutation operators, then mutation testing subsumes the criterion. Although mutation operators vary by language and mutation analysis tool, this section uses common operators that are used in most implementations. It is also possible to design mutation operators to force mutation to subsume other testing criteria. Further details are given in the bibliographic notes. This type of proof has one subtle problem. The condition coverage criteria impose only a local requirement; for example, edge coverage requires that each branch in the program be executed. Mutation, on the other hand, imposes global requirements in addition to local requirements. That is, mutation also requires that the mutant program produce incorrect output. For edge coverage, some specific mutants can be killed only if each branch is executed and the final output of the mutant is incorrect. On the one hand, this means that mutation imposes stronger requirements than the condition coverage criteria. On the other hand, and somewhat perversely, this also means that sometimes a test set that satisfies a coverage criteria will not kill all the associated mutants. Thus, mutation as defined earlier will not strictly subsume the condition coverage criteria. This problem is solved by basing the subsumptions on weak mutation. In terms of subsuming other coverage criteria, weak mutation only imposes the local requirements. In weak mutation, mutants that are not equivalent at the infection stage but are equivalent at the propagation stage (that is, an incorrect state is masked or repaired) are left in the set of test cases, so that edge coverage is subsumed. It is precisely the fact that such test cases are removed that strong mutation does not subsume edge coverage. Thus, this section shows that the coverage criteria are subsumed by weak mutation, not strong mutation. Subsumption is shown for graph coverage criteria from Chapter 2 and logic coverage criteria from Chapter 3. Some mutation operators only make sense for program source statements whereas others can apply to arbitrary structures such as logical expressions. For example, one common mutation operator is to replace statements with "bombs" that immediately cause the program to terminate execution or raise an exception. This mutation can only be defined for program statements. Another common mutation operator is to replace relational operators (, etc.) with other relational operators (the ROR operator). This kind of relational operator replacement can be applied to any logical expression, including guards FSMs. Node coverage requires each statement or basic block in the program to be executed. The mutation operator that replaces statements with "bombs" yields node coverage. To kill these mutants, we are required to find test cases that reach each
a∧b true ∧ b false ∧ b a ∧ true a ∧ false
(T T) T T F T F
(T F) F F F T F
(F T) F T F F F
(F F) F F F F F
Figure 5.3. Partial truth table for (a ∧ b).
basic block. Since this is exactly the requirement of node coverage, this operator yields node coverage and mutation subsumes node coverage. Edge coverage requires each edge in the control flow graph to be executed. A common mutation operator is to replace each predicate with both true and false (the ROR operator). To kill the true mutant, a test case must take the false branch, and to kill the false mutant, a test case must take the true branch. This operator forces each branch in the program to be executed, and thus it yields edge coverage and mutation subsumes edge coverage. Clause coverage requires each clause to become both true and false. The ROR, COR, and LOR mutation operators will together replace each clause in each predicate with both true and false. To kill the true mutant, a test case must cause the clause (and also the full predicate) to be false, and to kill the false mutant, a test case must cause the clause (and also the full predicate) to be true. This is exactly the requirement for clause coverage. A simple way to illustrate this is with a modified form of a truth table. Consider a predicate that has two clauses connected by an AND. Assume the predicate is (a ∧ b), where a and b are arbitrary boolean-valued clauses. The partial truth table in Figure 5.3 shows (a ∧ b) on the top line with the resulting value for each of the four combinations of values for a and b. Below the line are four mutations that replace each of a and b with true and false. To kill the mutants, the tester must choose an input (one of the four truth assignments on top of the table) that causes a result that is different from that of the original predicate. Consider mutant 1, tr ue ∧ b. Mutant 1 has the same result as the original clause for three of the four truth assignments. Thus, to kill that mutant, the tester must use a test case input value that causes the truth assignment (F T), as shown in the box. Likewise, mutant 3, a ∧ tr ue, can be killed only if the truth assignment (T F) is used. Thus, mutants 1 and 3 are killed if and only if clause coverage is satisfied, and the mutation operator yields clause coverage for this case. Note that mutants 2 and 4 are not needed to subsume clause coverage. Although the proof technique of showing that mutation operators yield clause coverage on a case-by-case basis with the logical operators is straightforward and relatively easy to grasp, it is clumsy. More generally, assume a predicate p and a clause a, and the clause coverage requirements to test p(a), which says that a must evaluate to both true and false. Consider the mutation p(a → true) (that is, the predicate where a is replaced by true). The only way to satisfy the infection condition for this mutant (and thus kill it) is to find a test case that causes a to take on the value of false. Likewise, the mutation p(a → false) can be killed only by a test case that
causes a to take on the value of true. Thus, in the general case, the mutation operator that replaces clauses with true and false yield clause coverage and is subsumed by mutation. Combinatorial coverage requires that the clauses in a predicate evaluate to each possible combination of truth values. In the general case combinatorial coverage has 2 N requirements for a predicate with N clauses. Since no single or combination of mutation operators produces 2 N mutants, it is easy to see that mutation cannot subsume COC. Active clause coverage requires that each clause c in a predicate p evaluates to true and false and determines the value of p. The first version in Chapter 3, general active clause coverage allows the values for other clauses in p to have different values when c is true and c is false. It is simple to show that mutation subsumes general active clause coverage; in fact, we already have. To kill the mutant p(a → true), we must satisfy the infection condition by causing p(a → true) to have a different value from p(a), that is, a must determine p. Likewise, to kill p(a → false), p(a → false) must have a different result from p(a), that is, a must determine p. Since this is exactly the requirement of GACC, this operator yields node coverage and mutation subsumes general active clause coverage. Note that this is only true if the incorrect value in the mutated program propagates to the end of the expression, which is one interpretation of weak mutation. Neither correlated active clause coverage nor restricted active clause coverage are subsumed by mutation operators. The reason is that both CACC and RACC require pairs of tests to have certain properties. In the case of CACC, the property is that the predicate outcome be different on the two tests associated with a particular clause. In the case of RACC, the property is that the minor clauses have exactly the same values on the two tests associated with a particular clause. Since each mutant is killed (or not) by a single test case, (as opposed to a pair of test cases), mutation analysis, at least as traditionally defined, cannot subsume criteria that impose relationships between pairs of test cases. Researchers have not determined whether mutation subsumes the inactive clause coverage criteria. All-defs data flow coverage requires that each definition of a variable reach at least one use. That is, for each definition of a variable X on node n, there must be a definition-clear subpath for X from n to a node or an edge with a use of X. The argument for subsumption is a little complicated for All-defs, and unlike the other arguments, All-defs requires that strong mutation be used. A common mutation operator is to delete statements with the goal of forcing each statement in the program to make an impact on the output.3 To show subsumption of All-defs, we restrict our attention to statements that contain variable definitions. Assume that the statement si contains a definition of a variable x, and mi is the mutant that deletes si ( si → null). To kill mi under strong mutation, a test case t must (1) cause the mutated statement to be reached (reachability), (2) cause the execution state of the program after execution of si to be incorrect (infection), and (3) cause the final output of the program to be incorrect (propagation). Any test case that reaches si will cause an incorrect execution state, because the mutated version of si will not assign a value to x. For the final output of the mutant to be incorrect, two cases occur. First, if x is an output variable, t must have caused
an execution of a subpath from the deleted definition of x to the output without an intervening definition (def-clear). Since the output is considered a use, this satisfies the criterion. Second, if x is not an output variable, then not defining x at si must result in an incorrect output state. This is possible only if x is used at some later point during execution without being redefined. Thus, t satisfies the all-defs criterion for the definition of x at si , and the mutation operator yields all-defs, ensuring that mutation subsumes all-defs. It is possible to design a mutation operator specifically to subsume all-uses, but such an operator has never been published or used in any tool.
EXERCISES Section 5.2. 1. Provide reachability conditions, infection conditions, propagation conditions, and test case values to kill mutants 2, 4, 5, and 6 in Figure 5.1. 2. Answer questions (a) through (d) for the mutant in the two methods, findVal() and sum(). (a) If possible, find a test input that does not reach the mutant. (b) If possible, find a test input that satisfies reachability but not infection for the mutant. (c) If possible, find a test input that satisfies infection, but not propagation for the mutant. (d) If possible, find a test input that kills mutant m. //Effects: If numbers null throw NullPointerException // else return LAST occurrence of val in numbers[] // If val not in numbers[] return -1 1. public static int findVal(int numbers[], int val) 2. { 3. int findVal = -1; 4. 5. for (int i=0; i
Report "Introduction to Software Testing"