Manoranjan Satpathy | Michael Butler | S. Ramesh | Michael Leuschel
Automatic Testing from Formal Specifications
TUCS Technical Report No 792, October 2006
Automatic Testing from Formal Specifications Manoranjan Satpathy ˚ Akademi University, Department of Information Technologies Abo Joukahaisenkatu 3-5, 20520 Turku, Finland
[email protected]
Michael Butler
University of Southampton, School of Electronic and Computer Science Highfield, Southampton, SO17 1BJ, UK
[email protected]
S. Ramesh
General Motors India Science Lab International Tech Park, Whitefield Road, Bangalore – 560066
[email protected]
Michael Leuschel
Heinrich-Heine Universitat Duesseldorf, Institute of Informatik Universitatsstr. 1, D-40225 Duesseldorf
[email protected]
TUCS Technical Report No 792, October 2006
Abstract In this article, we consider model oriented formal specification languages. We generate test cases by performing symbolic execution over a model, and from the test cases obtain a Java program. This Java program acts as a test driver and when it is run in conjunction with the implementation then testing is performed in an automatic manner. Our approach makes the testing cycle fully automatic. The main contribution of our work is that we perform automatic testing even when the models are non-deterministic. 1 2
Keywords: Model Based Testing; B-Method; Non-determinism
TUCS Laboratory Distribution Systems Design Laboratory, Embedded Systems Laboratory
1
This work was carried out as part of the EU research project IST 511599 RODIN (Rigorous Open Development Environment for Complex Systems); http://rodin.cs.nl.ac.uk 2 Part of this work was done when the first author was visiting General Motors India Science Lab, Bangalore during July-Sept 2005.
1 Introduction Software models are usually built to reduce the complexity of the development process and to ensure software quality. A model is usually a specification of the system which is developed from the requirements early in the development cycle [7]. In this paper, we consider model oriented formal specification languages like Z [31], VDM [14] , B [2] and ASM [11]. By model oriented we mean that system behaviour is described using an explicit model of the system state along with operations on the state. A formal model can be subjected to symbolic execution to obtain a coverage graph in which nodes represent instantiated states and edges are labeled with operation applications. One can then select a finite set of finite behaviours from the coverage graph and test if the implementation is consistent with these behaviours. This approach is often termed as model based testing [8, 26]. Model based testing though is an incomplete activity, the selected behaviours could be enriched to capture interesting aspects of the system and hence the success of their testing would give us confidence about the correctness of the system. In contrast to model based testing, there is code based testing in which the control structure of a program is analysed to generate a set of test inputs with the aim of covering the interesting aspects of the structure like all the statements, all the branches etc. There are similarities and differences between model based testing and code based testing. Model based testing can be thought of as addressing the question ’are the functionalities correct?’, whereas code based testing addresses the question of ’how much of software is being covered during testing?’ [24]. Both use coverage criteria, but in case of model based, it is the specification structure while for the latter, it is the code structure. In this article, we discuss automatic testing of an implementation or the system under test (SUT) written in accordance with a formal model in B [2]. We derive a test driver from the model which is essentially a program in the target language. If this driver is plugged into the implementation, then testing can be performed automatically. It invokes all of the test cases generated from the model and reports about their success or failure. Our method does not require the implementation source to be available, and the entire testing cycle is automatic. The main contribution of our work is that we perform automatic testing even when there is non-determinism in a model or in the implementation. In addition, our approach deals with deferred sets without offering them an explicit instantiation. The basic idea behind the handling of non-determinism can be seen from Figure 1. Assume the solid lines in the figure show the full state space of a model in which branchings may be due to non-deterministic choices. A correct implementation of this model must follow one of the paths in the figure. For testing purposes, we must know which path the SUT has taken, and then only we can match the properties of the path in the model with the path in the implementation to assign a verdict. In our strategy, we maintain a generic representation of the pos1
Root
A
Figure 1: Non-determinism Scenario sible paths that a model can take. Whenever the implementation makes a choice corresponding to a non-determinism in the model, we require that it makes this choice visible, and then this choice must satisfy a set of constraints which means that the implementation is not deviating from the model behaviour. Furthermore, the test driver uses the implementation choices to align the implementation trace with the appropriate trace of the model. Once this correspondence is established, additional properties could be checked with ease. The organization of this article is as follows. In the remaining part of this section we consider the testing terminology that we use. In Section 2, we discuss related work. Section 3 briefly describes the B notation along with an example. In Section 4, we consider our approach to handle deterministic models, and in Section 5, we extend the approach to non-deterministic models. Section 6 discusses the implementation issues along with the current status of our implementation. In Section 7, we outline the strengths and the weaknesses of our approach, and finally, Section 8 concludes the paper.
1.1 Terminology The following terminology on testing has been taken from Richardson et al. [28] and Zhu et al [34]. A testing criterion is a set of requirements on test data which reflects a notion of adequacy on the testing of a system. An adequacy criterion serves two purposes: (a) it defines a stopping rule which determines whether sufficient testing has already been done; so, testing can now be stopped, and (b) it provides measurements to obtain the degree of adequacy obtained after testing stopped. A test oracle is a mechanism to determine correctness of test executions. A test oracle has two components: oracle information and oracle procedure. The former specifies what constitutes correct behaviour, and the latter verifies the test 2
execution results in relation to the oracle information. A test driver is a tool which activates a system, provides test inputs and report test results [25]; in other words, a test driver sets up the target system’s initial state, inputs test data, invokes the system, captures the final state including the outputs, and finally, verifies the results of the test cases. Test cases that are derived from a model always refer to an abstract name space; therefore, it is necessary to map them to the concrete name space. Gannon et al. [9] have termed this as representation mapping. There are two kinds of mappings: control and data [28]. Control mappings are between control points in the implementation and locations in the specification; these are the points where the specification and the implementation states are to be matched. Data mappings are transformations between data structures in the implementation and those in the specification. Richardson in [27, 28], have considered query or probe functions for automating the conversion of concrete state of the system to abstract state. In addition, in their oracle formalism, they transform oracle information into assertions that must hold at specific control points. Yannakakis and Lee [33] classify a set of test cases into two categories: preset and adaptive. A test case on a finite state machine is a sequence of input symbols, and testing involves application of the sequence of input symbols and the observation of the sequence of output symbols. A test sequence is called preset if the input sequence is fixed prior to the start of testing; it is called adaptive if at each step the choice of the next input symbol depends on the observed previous outputs. The adaptive test cases are in the form of decision trees; the tester supplies an input and depending on the output, a branch is selected.
2 Related Work The work by Dick and Faivre [6] is a major contribution to the use of formal methods in software testing. A VDM [14] specification has state variables and an invariant (Inv) to restrict the variables. An operation, say OP, is specified by a pre-condition (OPpre ) and a post-condition (OPpost ). The expression OPpre ∧ OPpost ∧ Inv is converted into its Disjunctive Normal Form (DNF); each disjunct, unless a contradiction itself, represents an input sub-domain of OP. Next, as many operation instances are created as the number of valid disjuncts in the DNF. An attempt is then made to create a FSA (Finite State Automaton) in which each node represents a possible machine state and an edge represents an application of an operation instance. A set of test cases is then generated by traversing the FSA, each test case being a sequence of operation instances. BZ-Testing Tool (BZ-TT) [3, 4, 17] generates functional test cases from B as well as Z specifications. BZ-TT assumes all sets in the B machine are finite enumerated sets. Each B operation is transformed to its normal form [2]. An operation is then partitioned into a set of operation instances; each partition cor3
responds to exactly one control path within the operation. The conjunction of all predicates in a control flow path and the postcondition is termed an effect predicate (EP). The free state variables in each EP are assigned to their maximum and minimum values – say, in terms of size – to obtain a set of boundary goals. Boundary input values are obtained by giving maximum and minimum values to the input variables in the EP. A Constraint Logic Programming (CLP) Solver tries to find a path through symbolic execution from the initial state to a boundary state, a state satisfying a given boundary goal. And then relevant operation instances are applied at the boundary state by giving them boundary input values. The results of the query operations become the oracle information. BZ-TT assumes that the B machine is flat; i.e., without any hierarchy. Furthermore, all operations are deterministic. The authors point out that automatic verdict assignment is difficult because of non-determinism, and representation mappings [17, 18]. Satpathy et al. [29] discuss the prototype of a tool called ProTest which performs testing of an implementation in relation to its B model. The tool performs partition analysis using a technique similar to that of Dick and Faivre. A finite coverage graph is created from a symbolic execution of the B model by a model checking tool called ProB [20]. Some paths starting from the initial state are taken as test cases. The ProTest tool can run Java programs. So the B model and its Java implementation are run simultaneously by the tool, and in relation to a test case, similar states are matched to assign a verdict. The work also outlines some ideas concerning handling of non-determinism in B models.
3 The B-Method and Examples The B-method is a theory and methodology for formal development of computer systems [2]. The basic unit of specification in B is called a B machine. Larger specifications can be obtained by composing B machines in a (tree-like) hierarchical manner. An individual B machine consists of a set of variables, an invariant to restrict the variables, and a set of operations to modify the state. An operation has a precondition, and an operation invocation is defined only if the precondition holds. The initialization action and an operation body are written as atomic actions coded in a language called the generalized substitution language [2]. The language allows specification of deterministic and non-deterministic assignments and operations. An operation invocation transforms a machine state to a new state. The behaviour of a B machine can be described in terms of a sequence of operations, first operation of the sequence originating from the initial state of the machine. We will explain our method over two examples. Table 1 shows a simple travel agency machine which is deterministic (TAgency1.mch). There are two users (u1 and u2 in set USER) and two rooms (r1 and r2 in ROOM). The model can handle a single session at any time identified by s1 in SESSION. sess and booking 4
Machine TAgency1 SETS USER = {u1, u2}; SESSION= {s1}; ROOM= {r1, r2} VARIABLES sess, booking INVARIANT sess ∈ SESSION +→ USER /* partial function */ & booking∈ ROOM +→ USER INITIALISATION sess:= ∅ || booking := ∅ OPERATIONS login(u) = PRE u ∈ USER ∧ sess = ∅ THEN sess(s1) := u END; alloc(s)= PRE s ∈SESSION ∧sess 6= ∅ ∧ dom(booking) 6= {r1, r2} THEN IF r1 ∈ dom(booking) THEN booking(r2 ) := sess(s) ELSE booking(r1) := sess(s) END END; logout(s) = PRE s ∈SESSION∧sess 6= ∅ THEN sess := ∅ END END /* Probe Operations of login() */ uu ← login P1(s) = PRE s ∈ SESSION THEN uu := sess(s) END /* Probe Operations of alloc() */ uu ← alloc P1(s) = PRE s ∈ SESSION THEN uu := sess(s) END cc ← alloc P2() = BEGIN cc := card(booking) END Table 1: A Deterministic B machine are the two state variables; both of them are partial functions and initially empty. The login() operation assigns the single session to a user. Then alloc() can allocate a room, but preference is given to room r1. The logout() operation terminates the session. The machine TAgency2.mch shown in Appendix-A is a more complex version of TAgency1 and it involves non-determinism. The system is capable of handling a certain number of parallel sessions, which can be elements of a deferred set SESSION. Certain number of users can use the services of the system (enumerated set USER). A user can loin (operation login()) to get an available session which is non-deterministically allocated. The user can then make a request to book or unbook a room (book() and unbook()), and makes (or receives) payment through a card (enterCard()). Once payment or refund is made the user gets a response in relation to booking or unbooking (response book() and response unbook()). The set ROOM enumerates the individual rooms. If a room is allocated to a user, this information is stored in the function: booking. A user can book multiple rooms. Once a room has been successfully booked or unbooked, he can make futher requests under the current session after invoking again(). Operation logout() deallocates a session. This machine has four non-deterministic operations: enterCard(), retryCard(), response book() and response unbook(). For the first two operations, when the card is used for payment, a non-deterministic choice out of {valid, wrong} is made. For the re5
sponse book(), any room out of the set of available rooms is allocated. For the response unbook(), any one of the allocated rooms is deallocated.
4 The Method I: Deterministic Models We first explain our testing approach to handle deterministic machines. In the next phase we will illustrate the additional techniques to handle non-determinism. We make the following two assumptions: • We assume flat B machines; i.e. there would be no hierarchy. Though our technique can be extended to hierarchical B machines, it is not the current focus of this paper. • The implementation has the same signature as the specification. Java is the language of the implementation. We outline our testing method in the following steps. Next, we will explain the individual steps in greater detail. 1. Probe operation creation: For each operation, we create a set of probe operations. These operation results would be used as oracle information. 2. Signature file generation: The Java implementation has similar signature as that of the specification. 3. Creation of operation instances: The input space of a B operation will be partitioned to create operation instances. 4. Coverage Graph generation: A coverage graph is created by performing symbolic execution over the B machine. A Testing criterion decides to what extent the coverage graph is created. 5. Test case Generation: The Coverage graph will be traversed to generate a set of test cases which are obtained in relation to the testing criterion. 6. Test driver Generation: The test cases will be used to generate code in Java which would act as a test driver for the implementation.
4.1 Creation of Probe Operations For each operation in the B machine, a set of probe operations are manually created from the domain knowledge and the meaning of the operation. The probe operations will be used to match similar specification and implementation states at appropriate control points. Consider the operation call alloc(s). When a room has been allocated to a user, some of the possible queries could be as follows, and each of them becomes a probe operation of alloc(). 6
• Which user made the allocation request? (probe op alloc P1()) • How many rooms got allocated so far? (probe op alloc P2())
The probe operations are shown in Table 1. During model execution, these probe operations are to be called immediately after a call to alloc(s); therefore, the parameter of alloc() is available for them to use.
4.2 Signature Generation The SUT in Java must have a similar signature as the specification; i.e., the SUT will have the same operation names as those in the specification but their parameters would be similar to the model parameters in the following sense: • If a model parameter type is either numeric or boolean it is translated to types int and boolean in the implementation respectively. • For any other model parameter of type P P , we treat it as an object belonging to a class called P P in the implementation. For instance, the login() will have the signature ’void login(USER uu)’, and alloc() will have ’void alloc(ROOM rr)’. Note that we also create signatures of the probe operations; for instance alloc P2() will have the signature ’ROOM alloc P2(SESSION s)’ in the SUT. The SUT will implement SESSION, USER and ROOM as Java classes. It is expected that the developer would write the implementation in accordance with the specification and preserve the signatures of the original B operations and their probe operations. The signature generation process can easily be automated.
4.3 The Generation of Operation Instances Dick and Faivre [6] have suggested to partition the input space of an operation to create operation instances. However, B operations usually do have trivial preconditions and so may not generate interesting partitions. In order to address this issue, we do the following: • If an operation has an if-statement like ’IF c THEN S1 ELSE S2’, then add the tautology (c ∨ ¬c) to the precondition through conjunction. In case of nested ifs, say for ’IF c THEN (IF c1 THEN S1 ELSE S2) ELSE S2’, we add the tautology: (c ∧ (c1 ∨ ¬c1)) ∨ ¬c • If the operation has a SELECT with branch conditions c1 , . . . , ck then add through conjunction: c1 ∨ c2 ∨ . . . ck ∨ (¬(c1 ∨ c2 ∨ . . . ck )) 7
alloc1 (s): s ∈ SESSION ∧ sess 6= ∅ ∧ booking = ∅ alloc2 (s): s ∈ SESSION ∧ sess 6= ∅ ∧ booking 6= ∅∧ dom(booking) 6= {r1, r2} ∧ booking(r1 ) = sess(s) alloc3 (s): s ∈ SESSION ∧ sess 6= ∅ ∧ booking 6= ∅∧ dom(booking) 6= {r1, r2} ∧ booking(r1 ) 6= sess(s) Table 2: The instances of alloc() in TAgency1.mch • If set S occurs anywhere in the operation, then add through conjunction to the precondition the tautology: S = ∅ ∨ S 6= ∅ (One may add instead 0 (card(S) = 0 ∨ card(S) = 1 ∨ card(S) > 1) 0 to obtain finer partitions.) Depending on various data constructs in the model (like trees and sequences), we can add similar tautologies, but we don’t address them in this paper. We obtain the DNF of the modified precondition. The non-contradictory disjuncts are used for the creation of operation instances. Table 2 shows the three instances of operation alloc(). From now onwards, by operations we will mean operation instances.
4.4 Creation of a Coverage graph We construct a coverage graph by performing symbolic execution over the B model. Such a graph can be potentially infinite, but is usually limited by a bound on its depth or size. In addition, we use a testing criterion which may further limit the coverage graph. For the purpose of this paper, the testing criterion is to test each operation instance at least once. With this aim in mind, we try to generate a finite coverage graph so that each operation instance appears at least once. However, we may not be able to cover all operations because: (a) an operation may be infeasible, (b) a certain initialisation may prevent an operation from appearing or (c) an operation may not appear because of the finite construction. In this background, we outline our construction process in the following steps. • Step 1: Create an initial node (also called the root) in which the state variables of the B machine get the assignments of the INITIALISATION clause. This represents the initial state. • Step 2: Take any node in the graph and call it the source node (or state). Check if the precondition of a non-probe operation, say OP(), holds at this state. If so, apply this operation to obtain the target state. Create a new node for the target state only if a node with identical assignments does not already exist; otherwise consider the existing node. Label the edge joining the source and the target nodes with the operation invocation along with the results which the call may produce. 8
sess = {} booking = {}
INITIAL STATE
login(u2)
login(u1)
(login_P1(),u2) (login_P1(),u1)
sess = {(s1,u2)} booking = {}
sess = {(s1,u1)} booking = {}
alloc1(s1) alloc1(s1)
(alloc_P1(s1), u1) (alloc_P2(), 1)
(alloc_P1(s1), u2) (alloc_P2(), 1) sess = {(s1,u2)} booking = {(r1,u2)}
sess = {(s1,u1)} booking = {(r1,u1)}
alloc2(s1)
(alloc_P1(s1), u1) (alloc_P2(), 2)
sess = {(s1,u1)} booking = {(r1,u1),(r2,u1)}
Figure 2: A coverage graph for TAgency1.mch; dotted path shows one test case • Step 3: For each probe operation pop() of OP(), make a call to it at the target state to obtain the result res. Attach to the edge just created the pair (pop(), res). If testing criterion is satisfied stop; otherwise, jump to Step 2. Figure 2 shows a portion of a coverage graph generated out of TAgency1.mch. The probe operation calls and their results are shown within the dotted regions, and they have been attached to their respective edges. The result of a probe operation holds immediately after the application of the associated operation call. For instance, when we apply alloc2 (s1) as in Figure 2, the results of alloc P1(s1) and alloc P2(s1) are respectively u1 and r2. Observe how a probe operation may use the input or the result parameters of its associated call.
4.5 Generation of Test Sequences The algorithm in Table 3 takes a coverage graph and generates test cases with the intention of testing each operation instance which appears in the coverage graph. In Figure 2, the path shown by the dashed lines is a test sequence.
4.6 Generation of a test driver A test case is a path or an operation sequence in the graph. The test driver generation algorithm will take this operation sequence and generate a code fragment. This code fragment when executed in a testing context will infer whether the implementation has passed the test case. By testing context, we mean the following: 9
Algorithm: Generate-Test-Sequences input: A coverage graph with rt as the root. output: A set of basic test cases { Mark every edge in the graph as covered. while (there is an uncovered edges (N, N 0 ) with label OPj ) { Use Dijkstra’s algorithm to find shortest path p from rt to N Mark in graph the path from rt to N 0 through p as a test case. Mark all edges with label OPj as covered; for (each uncovered edge (Na , Nb ) with label OPab in p) Mark all edges with label OPab as covered; } } Table 3: Algorithm for generating Test Cases • If MM.mch is the name of the B machine, then the SUT declares a class with the name MM and creates an object of the same class, say mmo. It is expected that the class MM has all operations of the machine as methods with similar signatures. This is the job of the implementor. The constructor of class MM has initializations in accordance with the INITIALISATION clause of the B machine. • In any test case, if an operation has a parameter pp as an element of set PP, then SUT must have the object pp of class PP. • Local variables of appropriate types must be available to receive the results of the probe operations. Once we assume the testing context, the algorithm to generate the code fragment (part of the test driver) is trivial. Refer to Figure 3(a). (N, N 0 ) is an edge in the test sequence with 0 res ← OP (...)0 as the operation invocation. This edge has also k probe operation calls along with their result values obtained during symbolic execution. The corresponding code in Java has been shown in Figure 3(b). C[N ] stand for the code generated at node N . First a call to mmo.OP(...) is made. Then we obtain the results of the probe operations from the SUT which are compared with the results of the same operations stored in a test case; this comparison is performed by Java assertions. In (b), res 1,..., res k are the temporary variables to store the probe results. C[N 1] is the code generated at node N 1 which is obviously empty when N 1 is a terminal node. Given a test sequence, C[root] generates the code for the test case. Table 4 shows the code fragment in relation to the test case – marked by the dashed path – in Figure 2. It is assumed that TA1 is an object of class TAgency1 which corresponds to our machine TAgency1.mch. Note that the generation of code in Table 4 can easily be automated. 10
C [N] :
N res , where V ect is the state vector to store the bindings of expressions to state variables; these expressions may contain choice variables as sub-terms. AC is the set of accumulated constraints which essentially restricts the choice variables occurring in the expressions in V ect. AssP is an assertion which results from an application of a non-deterministic operation. For a node N , we refer to its fields by the dot notation such as N.V ect, N.AC etc. Figure 4 shows an edge in the coverage graph with appropriate labels, where < V ect, AC, AssP > constitute the source node. OP(X) is a call with X as the parameter list. Application of this call at the source node would give us the target node < V ect0 , AC 0 , AssP 0 >. The edge between the source and the target is labeled with < P, y ← OP (X) >. We will now define the method of obtaining y, P and the target node components. • P is a predicate to check that OP(X) is applicable at < V ect, AC, AssP >. If pre(OP (X)) is the precondition of OP(X) then P is an expression over the choice variables in V ect and is equivalent to AC ∧ pre(OP (X)) or its boolean simplification. We call it the Precondition Satisfaction Predicate (or PSP) which being false would mean that OP(X) is not applicable. • If OP() is a non-deterministic operation, y stands for the choice variable selected in place of the internal non-deterministic choice. If cc is the internal choice in OP(), and cpred is the choice predicate, then AssP 0 is the reduced form of the constraint cpred [y/cc]; i.e., the substitution of y in place of the free occurrences of cc in cpred. • V ect0 is the reduced form of V ect [body(OP (X))] where body(OP (X)) is the substitution in relation to OP (X) and it is performed over V ect. • AC 0 is the accumulated constraint of the target node and is equivalent to the reduced form of (AC ∧ P ∧ AssP 0 ). Note that AC 0 always includes AssP 0 . We maintain AssP 0 separately to be referred to by the test case generator. For the initial node in the coverage graph, AC is initialized to the constraints made out of the set declarations, and the constraint on the constants. Figure 5 shows a part of the coverage graph of the machine TAgency2.mch. The node marked ’1’ is the initial node. Its AC field is initialized to AC0 as given in the figure. The V ect field here corresponds to the INITIALIZATION clause of the machine. AssP is having the trivial value of true since no operation has been applied yet. Let us try to apply the operation instance login2 (u1) at the initial node; here the subscript is an operation instance tag. When the precondition pre(login 2 (u1)) is evaluated in the context AC0, it becomes true; therefore, the PSP is also true. The ANY statement within the operation means a non-deterministic choice; we 14
rstatus ← responseBook1 (sid) = PRE sid ∈ SESSION ∧ sid ∈ dom(session) ∧ s req(sid) = book∧ s state(sid) = s4 ∧ s card(sid) = valid∧ dom(booking) ⊂ (ROOM − nullR ) THEN s state(sid) := s5 || ANY rr WHERE rr∈ (ROOM − null r) − dom(booking) THEN booking(rr) := sess(sid) || rstatus := rr END END; Table 7: Operation instance: responseBook1 () select ZS1 as the choice variable. When the AssP field of node ’2’ is evaluated against AC0, it reduces to ZS1 ∈ SESSION . Now the new AC is AC0∧ZS1 ∈ SESSION . As a second example, consider the application responseBook 1 (ZS1) at the node marked ’3’ in the figure. Observe how the choice variable ZS1 has been used as a parameter here. The predicate AC3 ∧ pre(responseBook 1 (ZS1)) reduces to ZC1 = valid to become the PSP of the current invocation. If ZR1 is the choice variable in relation to the ANY statement, the expression (rr ∈ (ROOM − nullR ) − dom(booking)) [ZR1/rr] reduces to ZR1 ∈ (ROOM − nullR ) to becomes the AssP of the target node. Substitution of the operation body over the Vector of source node, gives us the new Vector. Finally, AC3 augmented with the PSP and the AssP becomes the AC of the target node.
5.3 Test cases from non-deterministic models As in the case of deterministic models, we use the algorithm in Table 3 to generate a set of linear test cases from the coverage graph which we now term as basic test cases. We term them basic because we will combine them to generate a set of adaptive test cases. If we treat a basic test case as a test case proper, then consider the case when SUT control encounters an edge with a non-trivial PSP and it does not hold. For example, in Figure 5, if edge < ZC1 = valid, ZC1 ← responseBook(ZS1) > occurs in a basic test case, then in reality ZC1 = wrong may be true and then there is no point in following this edge any further. In the worst case scenario, we may not be able to test any of the basic test cases into completion. Adaptive test cases are introduced precisely for this purpose. An Adaptive test case in the coverage graph is a subgraph in the form of a tree with the following properties: • Its root is same as the root of the coverage graph. 15
sess = {}; scard = {} sreq = {}; booking = {}; sstate={}
sess={(ZS1,u1)};scard={(ZS1,null)}
1
sreq={((ZS1,book)};sstate={(ZS1,s2)} booking={(ZR1, u1)} AC7 = AC6 true
AC0
true
(true, bookRoom_1(ZS1)) (true, ZS1