Contents
- Introduction
- The Application
- Implementing the Application
- Test Model
- Project Services
- Ceating a New Test Suite
- Loading a Test Case
- Performing Test Cases
- Matching States
- Report Models
- Producing HTML
Introduction
This tutorial shows how to develop an Eclipse application that integrates a number of XMF technologies with EMF. EMF is used to represent all models and XMF is used to perform all model execution.The application is a tool for performing model driven testing of a sustem under test (SUT). The test scripts and test reports are represented as instances of EMF models. XMF is used to design a domain specific language (DSL) for the testing language, to implement an execution engine for the testing language and to implement code templates that tansform test reports to HTML.
This tutorial is intended to show how XMF and EMF can be combined on Eclipse to produce a complete working application. It assumes that you are familiar with EMF and the basic concepts of XMF and XOCL.
To top.
The Application
Overview
Software systems execute by maintaining an internal state that is queried and modified by system functions that are exposed to the various users of the system. System design usually starts with a simple model of the internal state. Implementation and subsequent modification of the system represents the state information using programming language features that can be very different in structure and volume from the clean information model.
Once a system is implemented it must be tested. Testing involves checking that the system functions return expected information and perform the expected internal state modifications. If the correspondence between the state model and the state implementation is lost then testing must resort to manipulating the programming language constructs directly. This makes the test scripts brittle (they are sensitive to change in the implementation) and unnecessarily complex. Functional testing should test the logical implication of performing system functions and should not need to resort to the implementation detail.
Model Driven Testing is an approach to system testing that allows the test scripts to be constructed using the logical state model. The correspondence between the logical model and the implementation detail is retained, but is defined in one place instead of being spread out amongst the many test cases. Changes in implementation often do not cause any change in the state model (for example a new faster implementation for a table of records is discovered). Therefore, the information model changes much more slowly than its implementation making the test cases much less brittle.
An overview of the architecture of Model Driven Testing is shown in
the diagram below:
The system under test (SUT) exposes a collection of functions in an
interface that is available to clients. The interface is defined
against an information model that describes a logical view of the SUT
state. A testing framework is used to build and perform a collection of
test scripts. The test scripts are defined against the information
model.
Typically, a test script will perform a sequence of system functions
and then request the current state of the SUT. The state is returned as
an instance of the information model. The test script will then
interrogate the state to ensure that it contains the expected
information.
In another scenario, the testing framework can supply the SUT with an
instance of the information model in order to populate the current
state.
XMF-OS can be used to develop a Model Driven Testing Framework that
uses EMF to model the state information and to provide a simple
tree-based user interface. XMF-OS is used to define a testing language.
Test scripts written in the testing language can be loaded into the
browser and then performed against a SUT. The rest of this
section gives an overview of applying the intended application using a
Library example, the remainder of the article will focus on how the
application is built.
To top.
Using the application
The application of the testing application will be illustrated using
a library example. Assuming a simple information
system that implements a Library represented using the following model.
The model shown above shows a logical view of the Library information. A real Library Information System is likely to use a standard relational database to represent the information. When designing tests for the system it is much easier to think of the logical view rather then the implementation view. So long as the logical view can be translated into the implementation view and vice versa then no information is lost.
Given a logcial view of the information, the first step is to design some test cases. Our testing tool uses a domain specific language (DSL) for testing that makes writing test cases over information models easy. The testing DSL uses the idea of states and actions to construct test scripts. A test script connects to the SUT and sends requests to perform actions. An action is just a system function that is exposed to the testing framework by the SUT. Before and after performing actions, the test script may request the current state of the SUT. The state is returned (possibly by translating the implementation into a logical view) and matched against state patterns. The testing framework checks whether a state pattern is matched and changed the control flow in the test script accordingly.
Here is a very simple test script:
case LibraryTest {
// Define system states that will be used in the
// test scripts....
state hasBook(n)->(b) {
// State hasBook is satisfied when the
// current library state contains a book
// b with the title n. The state receives
// the name n and returns the book b...
obj(root,library.Library) {
books contains b;
}
obj(b,library.Book) {
title = n;
}
}
->
let () = match hasBook("Pride and Prejudice")
in
// If there is a book with the supplied title then
// raise an error...
raise bookExists("Not expecting a book.");
else {
// Do nothing...
}
}
Now, consider the state pattern named hasBook. A pattern contains a sequence of input parameters (in this case n) and a sequence of output parameters (in this case b). The idea is that a state pattern may be passed information, matches a pattern and then returns information extracted from the match. An SUT will always have a single root class that can be used to navigate via associations to all other classes. In the case of the Library information system, the root class is Library. When matching a state pattern, the variable 'root' is always associated with an instance of the root class. Each object in the state returned by the SUT can be matched using an object pattern of the form:
obj(var,class) {
slotName = slotValue;
slotName contains slotValue;
...
}
If the hasBook state pattern matches the supplied Library state then the Book instance is passed back as the value of the output parameter.
Here is the complete test case for the library:
case LibraryTest {
// Define system states that will be used in the
// test scripts....
state hasBook(n)->(b) {
// State hasBook is satisfied when the
// current library state contains a book
// b with the title n. The state receives
// the name n and returns the book b...
obj(root,library.Library) {
books contains b;
}
obj(b,library.Book) {
title = n;
}
}
state hasCopy(book,id) -> (copy) {
// State hasCopy is satisfied when the
// current library state has a book with
// the supplied id. the state returns the
// copy...
obj(root,library.Library) {
books contains book;
copies contains copy;
}
obj(copy,library.Copy) {
id = id;
book = book;
}
}
state hasReader(name) -> (reader) {
// State hasReader is satisfied when the
// current library state has a reader with
// the supplied name. The reader is
// returned...
obj(root,library.Library) {
readers contains reader;
}
obj(reader,library.Reader) {
name = name;
}
}
state borrows(reader,copy) -> () {
// State borrows is satisfied when the
// current library state records a borrowing
// event between the supplied reader and the
// supplied book copy. Nothing is returned...
obj(root,library.Library) {
borrowings contains b;
}
obj(b,library.Borrows) {
reader = reader;
copy = copy;
}
}
->
// The following test script is perfomed with respect
// to the declared system states above...
// Firstly, check that there is no book in the
// library with the supplied title...
let () = match hasBook("Pride and Prejudice")
in
// If there is a book with the supplied title then
// raise an error...
raise bookExists("Not expecting a book.");
else {
// Perform the system action that adds a book
// with the given title...
do addBook("Pride and Prejudice");
// Now use the same state check to ensure that
// the library state has been updated correctly...
let (book) = match hasBook("Pride and Prejudice")
in {
// If we get here then the library is OK.
// Now add a copy of the book...
do addCopy("Pride and Prejudice","copy1");
// Check that the library has recorded the copy...
let (copy) = match hasCopy(book,"copy1")
in {
// If we get here then the library has correctly
// recorded the copy of the book. Now add a reader
// with the given name...
do addReader("fred");
// Check that the reader was added...
let (reader) = match hasReader("fred")
in {
// If we get here then the library has a copy of
// the book and the reader is registered. Record
// the reader borrowing the book from the library...
do borrow("fred","copy1");
// Check that the borrowing has taken place...
let () = match borrows(reader,copy)
in {
// If we get here then everything has gone OK and
// we do nothing...
}
// The borrowing event was not recorded...
else raise noBorrow("fred","copy1");
}
// The reader was not registered...
else raise readerNotCreated("fred");
}
// A copy of the book was not recorded...
else raise noCopy("copy1");
}
// No book was added to the library...
else raise noBook("Pride and Prejudice.");
}
}
To use the tool a new Eclipse project "My Tests" is created, and a
new Testing Model added to
the project.
A new test suite is added to the model called "Library" via a right
click menu on Test Model.
Each test suite may contain any number of test cases. Each test case is
written in the testing DSL described above and is loaded from a text
file. A test script can be loaded via a menu attached to a test suite
in the browser. The "Library" test script is loaded which
consists of four state patterns and a script.
A test case can be performed with
respect to a SUT using a menu on the test case. When the test case is
performed, the script is executed causing a
sequence of action requests on the SUT. In addition to actions, the
state of the SUT is requested at appropriate times and matched against
the state patterns. The outcome of the match influences the control
flow through the test script. The result of performing a test case is
added as a report. Each time a test case is performed, a new report is
added to the test case.
A test case report consists of a sequence of steps. Each step is either an SUT action, a successful state match or a failed state match. A report can be displayed as a document via a menu.
Suppose that the SUT fails to add the reader named fred into the system correctly. When the testing tool requests the system state information model and matches it against the 'hasReader' state pattern, the match will fail and an appropriate exception should be raised. The resulting report shows that the SUT fails to behave as expected:
Implementing the Application
This section describes how the application described in the previous section is implemented using Eclipse, EMF and XMF-OS.To top.
Test Model
Test models are created as an instance of the following EMF meta-model.- NamedElement serves as an abstract super-class for all the elements in the model that have a name.
- TestModel is used as a container of test suites.
- TestSuite is a container of test cases.
- A test case has a number of state patterns, a single action and a collection of test reports. The idea is that a test case defines a script (the action) that performs functions of the SUT and checks the states of the SUT against the syate patterns. Each time the test case is performed, it produces a report that is saved as an instance of the class Report.
- The class Report is defined here.
- The class State is defined here.
- The class Action is an abstract super-class of the things that a test script can do.
- A Block is a sequence of actions that are performed in turn.
- The class Let is an abstract super-class of the actions that produce results. A let-action may be used to match against a state pattern or perform an SUT function.
- A MatchingLet is used to match the current state of the SUT against a named state pattern. At any time the SUT can be asked to produce a description of itself in terms of the information model. This description can be matched against a named state (possibly passing data values into the state). The match will either succeed or fail. If the match succeeds then 0 or more output values are produced. If the match fails then 0 output values are produced. A MatchingLet has a body that is performed if the match succeeds and an otherwise action that is performed if the match fails.
- A DoingLet performs an SUT function passing 0 or more input values and producing 0 or more output values. A DoingLet is equivalent to a Do, except a Do cannot get values back from the SUT.
- An exception is raised using the Raise class. The name of an exception is given along with some values.
- A Do is a request to perform an SUT function.
- Values may be passed as parameters to various actions. Values are represented as instances of LetArg. A LetArg may be a constant LetConst or a variable reference LetVarRef.
To top.
Project Services
When developing an XMF engine for an Eclipse plugin, a key feature is the design of the services-interface. The services are the public interface privided by the XMF application to the Eclipse framework. The services are named operations that are implemented in XOCL and can be called by Java. A typical use of services is to link them to Eclipse menu items so that they can be called via the GUI. The testing framework is to provide the following services:- newTestSuite(testModel)
This service will request the name of a new test suite from the user, create a test suite with the supplied name and then add the new test suite to the supplied test model.
- loadTestCase(testSuite)
This service will request a file from the user. The file should contain the source code for a test case. The contents of the file will be loaded and the appropriate test case created and added to the supplied test suite.
- perform(testCase,library)
The supplied test case is performed with respect to the current SUT supplied as an argument. For the purposes of the tutorial, the service will create a new library and perform the supplied test case with respect to that. The result of performing the test case will be a report that is added to the reports currently recorded against the test case.
- report(report)
This service translates the supplied test report to HTML and displays it in a browser.
Firstly, the services are defined as follows:
@Service newTestSuite(testModel)
let name = xmf.getString("Name","Name of new Test Suite","")
in testModel.testSuites.add(TestSuite(name))
end
end;
@Service loadTestCase(testSuite)
// Use a try to catch any parsing errors that
// might be raised and then use a dialog to report
// the error...
try
testSuite.loadTestCase(xmf.openFile(".","*.txt",""))
catch(x)
xmf.reportError(x.message)
end
end;
@Service perform(testCase,library)
testCase.perform(library)
end;
@Service report(report)
xmf.browser("Report",report.report())
end;
<extension point="org.eclipse.ui.popupMenus">
<objectContribution id="id" objectClass="testing.TestModel">
<action id="newTestSuite" label="New Test Suite" menubarPath="additions" class="actions.HandleAction" enablesFor="1">
</action>
</objectContribution>
</extension>
<extension point="org.eclipse.ui.popupMenus">
<objectContribution id="id" objectClass="testing.TestSuite">
<action id="loadTestCase" label="Load Test Case" menubarPath="additions" class="actions.HandleAction" enablesFor="1">
</action>
</objectContribution>
</extension>
<extension point="org.eclipse.ui.popupMenus">
<objectContribution id="id" objectClass="testing.TestCase">
<action id="perform" label="Perform" menubarPath="additions" class="actions.HandleAction" enablesFor="1">
</action>
</objectContribution>
</extension>
<extension point="org.eclipse.ui.popupMenus">
<objectContribution id="id" objectClass="reports.Report">
<action id="report" label="Report" menubarPath="additions" class="actions.HandleAction" enablesFor="1">
</action>
</objectContribution>
</extension>
public void run(IAction action) {
XMFMachine machine = XMFMachineRegistry.getMachine("testing.machine");
String id = action.getId();
Object o = selection.getFirstElement();
ClassLoader loader = classLoader();
if (id.equals("newTestSuite"))
machine.sendService("newTestSuite", new Object[] { o },loader);
else if (id.equals("loadTestCase"))
machine.sendService("loadTestCase", new Object[] { o },loader);
else if (id.equals("perform")) {
Library library = LibraryFactoryImpl.eINSTANCE.createLibrary();
machine.sendService("perform", new Object[] { o, library },loader);
} else if (id.equals("report"))
machine.sendService("report", new Object[] { o },loader);
else System.out.println("Unknown action id: " + id);
}
To top.
Creating a New Test Suite
The new test suite service, simply creates and adds a new test suite to the supplied test model. It uses a dialog: xmf.getString() to request the name of the new suite from the user.To top.
Loading a Test Case
A new test case is loaded from a file. The dialog, xmf.openFile() is
used to request the path to the file containing the source code for the
test case. The source code is loaded via the loadTestCase operation
defined for TestSuite:context TestSuite
@Operation loadTestCase(path:String)
// Use the Grammar::parseFile operation to parse the contents
// of the file, starting at the non-terminal TestCase...
self.getTestCases().add(TestCase.grammar.parseFile(path,"TestCase",Seq{}))
end
@Grammar extends State.grammar
TestCase ::=
// A test case is named...
'case' n = Name '{'
// has a sequence of state patterns...
S = State*
'->'
// a test script action...
a = Action
'}'
{ // Create the test script...
let t = TestCase(n,a)
in
// Add the states into the script...
@For s in S do
t.getStates().add(s)
end;
t
end
}.
Action ::=
// An action is one of...
Do // Call an SUT function...
| Block // Perform a sequence of actions...
| Let // Bind some variables...
| Raise. // Raise an exception.
Condition ::= Str.
Do ::=
// Perform a named SUT function...
'do' n = Name A = LetArgs ';' {
// Create the Do...
let d = Do(n)
in
// Add in the argument values...
@For a in A do
d.getArgs().add(a)
end;
d
end
}.
Block ::=
// A block is a sequence of actions...
'{' as = Action* '}'
// Create the block an add in the actions...
{ let b = Block()
in @For a in as do
b.getActions().add(a)
end;
b
end
}.
Let ::=
// All types of let-action bind 0 or more
// arguments. the let-actions differ in terms
// of where the variable values some from...
'let' V = LetVars '=' LetBind^(V).
LetBind(V) ::=
// Either a state match or an SUT function...
MatchingLet^(V)
| DoingLet^(V).
MatchingLet(V) ::=
// Name the state pattern and supply any input
// values..
'match'
n = Name
A = LetArgs
// Specify the action to perform when the
// match is successful...
'in' a = Action
// Specify the action to perform when the
// match fails...
'else' b = Action
{
// Create the matching-let and populate the
// apropriate parts...
let l = MatchingLet(n,a,b)
in @For v in V do
l.getBindings().add(v)
end;
@For a in A do
l.getArgs().add(a)
end;
l
end
}.
DoingLet(V) ::=
// Name the SUT function and supply the values...
'do' n = Name A = LetArgs
// Perform a body for the variables...
'in' a = Action {
// Create the let and populate it...
let l = DoingLet(n,a)
in @For v in V do
l.getBindings().add(v)
end;
@For a in A do
l.getArgs().add(a)
end;
l
end
}.
LetVars ::= '(' LetVarsTail.
LetVarsTail ::=
n = Name ns = (',' Name)* ')' { Seq{n|ns}->collect(n | LetVar(n)) }
| ')' { Seq{} }.
LetArgs ::= '(' LetArgsTail.
LetArgsTail ::=
a = LetArg as = (',' LetArg)* ')' { Seq{a|as} }
| ')' { Seq{} }.
LetArg ::= LetVarRef | LetConst.
LetVarRef ::= n = Name { LetVarRef(n) }.
LetConst ::= LetInt | LetStr.
LetInt ::= i = Int { LetInt(i) }.
LetStr ::= s = Str { LetStr(s) }.
Raise ::= 'raise' n = Name A = LetArgs ';' {
let r = Raise(n)
in @For a in A do
r.getArgs().add(a)
end;
r
end
}.
end
To top.
Performing Test Cases
A test case is performed by supplying it with an SUT state description (in this case the current EMF Library instance) and executing the body of the test case. There are two key aspects to test case execution: performing the test case actions; and matching SUT states against the state patterns. This section describes how test case actions are performed (making reference to state matching) and the next section defines how states are matched.A test case is supplied with an SUT state and performs each of the actions with respect to:
- The SUT state.
- A variable environment that associates names with values. The environment is just a collection of Seq{name | value} pairs. The variables are bound via the let-actions and the state matching.
- A state environment that associates names with states defined by the test case.
- A report that is used to document the steps taken by the actions in the test case.
- A continuation. The continuation is an operation that is used by each action to pass control to the next action to be performed. If for any reason an action wishes to abort control (for example when an exception is raised) the continuation is ignored, thereby jumping out of the normal sequence of control.
The entry point for performing test case actions is defined as follows:
@Operation perform(state)
// This is the entry point from the service...
self.perform(Seq{},state)
end
@Operation perform(env,state)
// Supplied with the variable environment and the SUT
// state. Create a new report, create a state environment
// then call the root action with the arguments...
let report = Report(name,Date())
in self.getReports().add(report);
action.perform(env,self.stateEnv(),state,report,
@Operation(varEnv,state)
varEnv
end)
end
end
@Operation stateEnv()
// Calculate a state environment...
states.asSeq()->iterate(s e = Seq{} | e.bind(s.getName(),s))
end
- Blocks, step through the actions. Control is implemented via the
continuation in case any of the actions wish to abort:
@Operation perform(varEnv,stateEnv,currentState,report,succ)
self.perform(actions.asSeq(),varEnv,stateEnv,currentState,report,succ)
end
@Operation perform(actions,varEnv,stateEnv,currentState,report,succ)
if actions->isEmpty
then succ(varEnv,currentState)
else actions->head.perform(varEnv,stateEnv,currentState,report,
@Operation(varEnv,currentState)
self.perform(actions->tail,varEnv,stateEnv,currentState,report,succ)
end)
end
end
- Do actions call a function in the
interface of the SUT:
@Operation perform(varEnv,stateEnv,currentState,report,succ)
let values = args.asSeq()->collect(arg | arg.eval(varEnv));
action = Action(actionName)
in currentState.send(actionName,values);
@For v in values do
action.getArgs().add(Value(v))
end;
report.getSteps().add(action);
succ(varEnv,currentState)
end
end
- MatchingLet performs a match against a
named state. If the match suceeds control goes one way and if it fails
then control goes another way:
@Operation perform(varEnv,stateEnv,currentState,report,succ)
if stateEnv.binds(state)
then
let s = stateEnv.lookup(state);
inValues = args.asSeq()->collect(arg | arg.eval(varEnv))
in
// The mechanics of matching is described below, essentially it
// takes a variable environment, a success and a fail continuation.
// If the match succeeds then the success continuation is
// eventually called, otherwise the fail continuation is
// called...
s.match(
// The variable environment...
s.bindArgs(currentState,inValues),
// The success continuation for the match...
@Operation(env,fail)
// The match succeeded! Get the output values from
// the state and bind them to the values specified
// in the let, then carry on with the body of the
// let...
let outValues = s.getOutValues(env);
newEnv = varEnv;
step = Match(state)
in
// Create a new environment by binding the names
// introduced by the let to the values supplied by
// the out parameters of the state pattern...
@For binding,value in bindings.asSeq(),outValues do
newEnv := newEnv.bind(binding.getName(),value)
end;
// Record the ins and outs in the report step...
@For v in inValues do
step.getIns().add(Value(v))
end;
@For v in outValues do
step.getOuts().add(Value(v))
end;
// Update the report...
report.getSteps().add(step);
// Continue with the body of the let...
body.perform(newEnv,stateEnv,currentState,report,
@Operation(ignoreEnv,currentState)
succ(varEnv,currentState)
end)
end
end,
// The fail continuation for the match...
@Operation()
// The match failed! Record this in the report and
// then continue with the otherwise part of the let...
let step = MatchFails(state)
in @For v in inValues do
step.getIns().add(Value(v))
end;
report.getSteps().add(step);
otherwise.perform(varEnv,stateEnv,currentState,report,succ)
end
end)
end
else self.error("Cannot find state named " + state)
end
end - Raise causes control to abort (the success continuation is
ignored):
@Operation perform(varEnv,stateEnv,currentState,report,succ)
let step = Exception(exception)
in report.getSteps().add(step)
end
end
To top.
Matching States
State patterns are used to describe snapshots of an SUT. The pattern
consists of a collection of objects. Each object has a type and has
slots whose values may be simple values, collections of values or
references to other objects. Each slot has a unique identity. A state
pattern may contain variables where the variable represents any value. An SUT state is matched against a state pattern. If the SUT state contains objects of the corresponding types and whose slot values match the those in the pattern then the SUT state matches the pattern. Any variables in the pattern will match any corresponding values in the SUT state and an environment of variable bindings is produced as a side effect of the match.
The EMF state pattern model is shown below:
- A State consists of input and output variables and a collection of objects. When a state is matched it is supplied with the input variables. If the pattern matches the SUT state then an environment of variable bindings is produced as a side-effect. The output variables are a sub-set of the variables in the state pattern and are returned to the caller. In order for the state to match, all of the objects in the body of the state pattern must match corresponding objects in the SUT state.
- An Object has a type (an instance of Path which designates a Java
class) and a collection of slots. The name of an object is its
identity. Multiple occurrences of the same object identity name must
refer to the same object in the SUT state.
- A Slot has a name and a collection of values. A slot is either a SlotEquals or a SlotContains. The value pattern of a SlotEquals pattern must match the value in the corresponding SUT slot. The value pattern of a SlotContains pattern must match against one of the elements in the collection found in the SUT slot.
- Values can be constants (strings, integers or floats), may be sequences (defines with heads and tails) or may be specified as variables. Constants and sequences must match the equivalent SUT element. Variables match any SUT element.
State matching is performed by a match operation defined by State as follows:
@Operation match(env,succ,fail)
// Supply a collection of objects that must match.
// If all the objects match then invoke the succ continuation
// Otherwise invoke the fail continuation. The supplied
// environment associates variable names with values. The
// initial call to match associates the name 'main' with
// a root SUT element...
self.match(objects.asSeq(),env,succ,fail)
end
@Operation match(objects,env,succ,fail)
// All of the objects must match in order for the
// succ continuation to be called...
@Find(o,objects)
// Get one of the outstanding objects to be matched
// against the SUT...
when env.binds(o.getName())
do
// We have an object pattern o whose name is bound to
// an SUT object in the environment. Match the pattern
// to the SUT object...
o.match(env.lookup(o.getName()),env,
// The success continuation calls match again with
// one less objct pattern to match...
@Operation matchSucc(env,fail)
self.match(objects->excluding(o),env,succ,fail)
end,
fail)
else succ(env,fail)
end
end
@Operation match(value,env,succ,fail)
// Check that the supplied SUT value is an instance of the
// Java class designated by the path of the object pattern...
if path.getType().isInstance(value)
then self.matchSlots(slots.asSeq(),value,env->bind(name,value),succ,fail)
else fail()
end
end
@Operation matchSlots(slots,value,env,succ,fail)
// All of the slots are tried in turn. Once they have
// all succeeded then the match succeeds...
if slots->isEmpty
then succ(env,fail)
else
// Try a slot...
let slot = slots->head
in
// The SUT value must have the slot...
if value.hasSlot(slot.getName())
then
// Match the slot-pattern value with the SUT object
// value. Pass a success continuation that tries the
// rest of the slots...
slot.match(value.get(slot.getName()),env,
@Operation(env,fail)
self.matchSlots(slots->tail,value,env,succ,fail)
end,
fail)
else fail()
end
end
end
end
@Operation match(x,env,succ,fail)
value.match(x,env,succ,fail)
end
@Operation match(v,env,succ,fail)
// Check that the supplied element is an EList or a sequence of
// elements...
if xmf.javaClass("org.eclipse.emf.common.util.EList").isInstance(v)
then self.matchSequence(v.asSeq(),env,succ,fail)
elseif v.isKindOf(SeqOfElement)
then self.matchSequence(v,env,succ,fail)
else fail()
end
end
@Operation matchSequence(seq,env,succ,fail)
// *ONE* of the elements of the supplied sequence must match
// in order for the success continuation to be invoked...
if seq->isEmpty
then
// No element matched...
fail()
else value.match(seq->head,env,succ,
// If the match ever fails later on then the failure continuation
// will cause another element of the supplied sequence to be
// selected and tried...
@Operation matchSequenceFail()
self.matchSequence(seq->tail,env,succ,fail)
end)
end
end
@Operation match(x,env,succ,fail)
if x = value
then succ(env,fail)
else fail()
end
end
To top.
Report Models
- A Report consists of a number of steps in order.
- A Match step records the successful match of a state pattern to an SUT state. The name of the match is that of the state pattern and the ins and outs of the step are the values supplied and produced by the pattern match.
- An Action step records the invocation of a named SUT function. The arguments supplied to the SUT function are recorded in the step.
- A MatchFails records the failure of an attempted state pattern match. The name of the state pattern and the supplied arguments are recorded by the step.
- An Exception step records an exception that was raised by the test script. The name of the exception and its argument values are recorded in the step.
To top.
Producing HTML from Reports
@HTML(out,0)
some HTML text...
end
@HTML(out,0)
<b> { self.name() } </b>
end
Furthermore, if [ and ] are encountered within { and } then the text between [ and ] is treated as literal HTML and is written to the output. Therefore,
@HTML(out,0)
<b> { self.name() [ and ] other.name() } </b>
end
The rest of this section describes the report generation templates for the Report model. The class Report produces HTML as follows:
context Report
@Operation report(out:OutputChannel)
@HTML(out,0)
<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.0 Transitional//EN\">
<HTML>
<BODY BGCOLOR="#FDF5E6">
<H1 ALIGN=\"CENTER\"> { testCase } </H1>
Generated on { date.toString() }
<BR>
// Set up a table to contain the steps.
// Each step will be a new row in the table...
<TABLE>
{ @For step in self.getSteps() do
step.report(out)
e_nd
}
</TABLE>
</BODY>
</HTML>
end
end
context Match
@Operation report(out:OutputChannel)
@HTML(out,0)
<TR>
<TD> { name } matched. </TD>
<TD> { self.reportValues(ins,out) } </TD>
<TD> { self.reportValues(outs,out) } </TD>
<TD> State </TD>
</TR>
end
end
context Match
@Operation reportValues(values,out:OutputChannel)
@HTML(out,0)
(
{ @For value in values do
[ { value.value.toString() } ];
if not isLast
then [,]
e_nd
e_nd
}
)
end
end
is as follows:
<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.0 Transitional//EN\">
<HTML>
<BODY BGCOLOR="#FDF5E6">
<H1 ALIGN=\"CENTER\"> example1 </H1>
Generated on Thu Dec 13 16:26:13 GMT 2007
<BR>
<TABLE>
hasBook failed to match with args: (
Pride and Predjudice
)
<TR>
<TD> Called addBook </TD>
<TD>
Value(Pride and Predjudice)
</TD>
<TD> </TD>
<TD> State </TD>
</TR>
<TR>
<TD> hasBook matched. </TD>
<TD>
(
Pride and Predjudice
)
</TD>
<TD>
(
Book(Pride and Predjudice)
)
</TD>
<TD> State </TD>
</TR>
<TR>
<TD> Called addCopy </TD>
<TD>
Value(Pride and Predjudice) Value(copy1)
</TD>
<TD> </TD>
<TD> State </TD>
</TR>
<TR>
<TD> hasCopy matched. </TD>
<TD>
(
Book(Pride and Predjudice) , copy1
)
</TD>
<TD>
(
Copy(Book(Pride and Predjudice),copy1)
)
</TD>
<TD> State </TD>
</TR>
<TR>
<TD> Called addReader </TD>
<TD>
Value(fred)
</TD>
<TD> </TD>
<TD> State </TD>
</TR>
<TR>
<TD> hasReader matched. </TD>
<TD>
(
fred
)
</TD>
<TD>
(
Reader(fred)
)
</TD>
<TD> State </TD>
</TR>
<TR>
<TD> Called borrow </TD>
<TD>
Value(fred) Value(copy1)
</TD>
<TD> </TD>
<TD> State </TD>
</TR>
<TR>
<TD> borrows matched. </TD>
<TD>
(
Reader(fred) , Copy(Book(Pride and Predjudice),copy1)
)
</TD>
<TD>
(
)
</TD>
<TD> State </TD>
</TR>
</TABLE>
</BODY>
</HTML>
To top.