While section "SCTUnit by example" gave an overview of test classes and operation, the section at hand provides you with a complete description of the SCTUnit language. Since the SCTUnit language is an extension of the statechart language, this section will focus on language elements that are specific to the SCTUnit language. Please see section "The statechart language" for everything else.
An important extension of the SCTUnit language over the statechart language is that you can write your own operations (subroutines) and control statements, like if and while . They make it possible to “script” complex procedures, for example by raising different events under different conditions or by looping over a sequence of statements multiple times.
Please remember that you can always hit [Ctrl]+[Space]
when editing SCTUnit language files within itemis CREATE. The editor will show you all possible input choices that are valid at your cursor location.
A test class contains one or more operations or test cases. It is contained in a file with a .sctunit filename extension.
Example:
testclass NameOfTheTestClass for statechart NameOfTheStatechart {
const pi : real = 3.141592654
var sum : integer = 0
@Test
operation isFeatureAvailable () {
/* … */
}
}
The
header of a test class consists of the keyword
testclass
, followed by the name of the test class, followed by the keywords
for statechart
, followed by the name of the statechart this test class relates to.
The test class imports the statechart, so all variables, operations, events, etc. that are defined in an interface in the statechart’s definition section, as well as the statechart’s states, are available in and accessible by the test class.
Please note: Entities defined in a statechart’s internal scope are not visible from the outside, including test classes controlling the statechart.
The
body of a test class is enclosed in braces ({ … }
). It consists of an optional part with variable and constant definitions, followed by at least one
operation.
Figure "Test class grammar" summarizes the structure of a test class:
Test class grammar
Test classes are namespace-aware: Using the package statement , you can insert a test class into a specific namespace.
Test classes can be grouped into a test suite.
Operations are comparable to methods, functions, or subroutines in other programming languages. Operations are defined in test classes.
A
test is parameterless operation without a return type that is annotated with @Test
.
Example:
@Test
operation isFeatureAvailable () {
enter
var i: integer = 0
var countValue: integer = count
while (i < 10) {
raise do_cycle
assert active (myStatechart.main_region.State_A)
assert count == countValue + 1
i = i + 1
}
}
The
header of an operation consists of the keyword
operation
, followed by the name of the operation, followed by a parenthesized list of parameters, optionally followed by a colon and a return type. If no return type is specified, it is inferred from the operation’s
return statement(s)
.
Operations can also be annotated. Operations annotated with
Test
are automatically executed when the SCTUnit test is executed.
The
body of an operation is enclosed in braces ({ … }
). It consists of a sequence of
statements, which may be empty. If the operation’s header specifies a return type different from
void
, the operation must return a value of that type, using the
return statement
.
Figure "Operation grammar" summarizes the structure of an operation:
Operation grammar
An operation can define its own variables. Aside from these
it also has access to
An operation can call other operations. This is like subroutine calls in other programming languages.
Operations that are declared in the statechart cannot be called from a test class.
When running a
test class or a
test suite as an SCTUnit, only those operations are executed as tests that are annotated with @Test
. Operations annotated with @Ignore
or not annotated at all will not be regarded as tests. However, they can be called by test operations.
While the @Ignore
annotation and an omitted annotation have the same effect functionally, you should use the @Ignore
annotation to mark operations that are intended as tests, but are (temporarily) disabled for one or the other reason. This differentiates them from other operations that are not tests, but mere subroutines called from elsewhere.
The body of an operation consists of statements. Many types of statements, like variable definitions, assignments, event raisings, or operation calls, are already defined in the statechart language and are described in section "Statements" of the statechart language documentation.
Statement types that are specific to the SCTUnit language are described in the following subsections.
The return statement terminates the execution of the current operation. It either returns nothing or the value of an expression to the caller. If an expression is returned, its type must match the operation’s return type. If the operation returns nothing, its return type must be void, either implicitly or explicitly.
Example:
operation getCircleArea (radius: real): real {
const pi: real = 3.141592654
return pi * pi * radius
}
The
return statement evaluates the expression
pi * pi * radius
, i.e., the area of a circle with radius
radius, and returns the result to the caller of the operation. .
Figure "Return statement grammar" summarizes the structure of the return statement:
Return statement grammar
active is a built-in function of the statechart language. In the context of testing, it can be used to determine a state’s state at a given time. It returns a logic value, if the state in question is active it returns true, otherwise false.
Example:
assert active (myStatechart.main_region.State_A)
The first statement asserts that state State_A in region main_region in statechart myStatechart is active. If this is not the case, the test fails. You can read about assertion in the next chapter.
When it comes to testing, the most important statement is the assertion. It evaluates a condition that must be fulfilled for the test to not fail.
Example 1:
assert sum == 42
This statement asserts that the variable sum has a value of 42. If this is the case, the operation continues. If not, the test fails and is stopped.
Unlike some other test frameworks, SCTUnit does not differentiate between
assert
andassert fatal
or similar. All assertions are “fatal”, which means they stop the test when they fail.
Example 2:
assert active (myStatechart.main_region.State_A)
assert !active (myStatechart.main_region.State_B) message "State_B must not be active here."
The first statement asserts that state State_A in region main_region in statechart myStatechart is active. If this is not the case, the test fails.
The second statement asserts that State_B is not active. Otherwise the test fails with the error message “State_B must not be active here.”.
Please note: active(…) is a built-in function of the statechart language.
Generally, an assertion consists of the keyword
assert
, followed by a boolean expression, optionally followed by the keyword
message
and an error message text (string literal). The assertion expects the boolean expression to be
true to continue the test. If it evaluates to
false the test fails. The optional message can be used to clarify what went wrong.
A special assertion variant uses the
called
keyword ("
assert called statement"). It checks whether a certain operation has been called (executed), typically by some action in the statechart.
Example:
assert called myOperation
assert called myOperation(42, 815)
assert called myOperation 4 times
assert called myOperation(42, 815) 1 times
assert ! called myError
The first assertion checks whether the operation myOperation has been called during the execution of this test. If it hasn’t, the test fails.
The second assertion not only checks whether the operation
myOperation has been called, but also checks whether it has been called with parameters 42 and 815. If the operation hasn’t been called at all, the test fails. The test also fails if the operation has been called, but with different parameters than 42 and 815, e.g.,
myOperation(1, 2)
.
The third assertion checks whether the operation myOperation has been called at least 4 times, no matter the arguments, while the fourth assertion checks if the operation has been called at least one time with the specified parameters.
The fifth assertion checks if the “forbidden” operation
myError
has been called, and fails in that case.
Finally, you can use the
assert statement to check whether the state machine has raised an outgoing event. To do so, the
assert
keyword is followed by the name of the desired event. It is also possible to assert the opposite, i.e., that the event has
not been raised. In this case, insert the negation operator
!
between
assert
and the event name.
Here’s an example:
The statechart below transitions from state A to state B on either the e1 or the e2 incoming event. However, on e1, the outgoing event e3 will be raised, while this is not the case on e2.
Statechart raising an outgoing event
You can verify this behavior using the following SCTUnit test class. The statement
assert e3
succeeds if the e3 event has been raised, while
assert ! e3
succeeds if that event has not been raised.
testclass outgoingEventTest for statechart raiseOutgoingEvent {
@Test
operation test_e1 () {
enter
raise e1
proceed 1 cycle
assert e3
exit
}
@Test
operation test_e2 () {
enter
raise e2
proceed 1 cycle
assert ! e3
exit
}
}
Figure "Assertion grammar" summarizes the structure of an assertion:
Assertion grammar
The enter statement serves to enter the statechart associated with this test class. The state machine is initialized and started. The state that is denoted by the initial state becomes active.
A test must execute the enter statement before it can perform any sensible testing on the statechart. Unless the state machine is entered, all states are inactive.
Please see section "SCTUnit by example" for examples on how the enter statement is used. Please also see the section on the exit statement .
The exit statement exits and quits a state machine. You can re-initialize and re-enter it using the enter statement .
Example:
The statechart myStatechart looks like this:
Statechart myStatechart
The enter and exit statements are explained by the comments of this test class:
testclass enter_exit_tests for statechart myStatechart {
@Test
operation checkState () {
/* Before entering the state machine, all states are inactive: */
assert !active (myStatechart.main_region.State_A)
/* Now we are entering the state machine. The state that the
* initial state points to becomes active: */
enter
assert active (myStatechart.main_region.State_A)
/* The "exit" statement leaves the state machine. All of the
* latter's states are thus inactive:
*/
exit
assert !active (myStatechart.main_region.State_A)
/* It is possible to re-enter the state machine or to be precise:
* to enter a new instance of the state machine. As above,
* "State_A" should be active now: */
enter
assert active (myStatechart.main_region.State_A)
}
}
The raise statement raises one of the state machine’s incoming events.
Example 1:
raise operate
This statement raises the operate event, defined in the state machine’s default interface.
Example 2:
raise valueChanged : 3
This statement raises the valueChanged event, defined in the state machine’s default interface, which is of type integer. Raising a typed event without a payload is not allowed.
Example 3:
raise user.click
This statement raises the click event, defined in the state machine’s user interface.
Since state machine internals are inaccessible to SCTUnit tests and outgoing events cannot be raised in general, only incoming events can be raised. Internal events, defined in the internal scope, and outgoing events, defined in interfaces, cannot be raised.
Please note: The raise statement is not specific to the SCTUnit language, but is (also) part of the statechart language, see section "Raising an event".
The proceed statement allows to proceed the state machine either in terms of time or in terms of run-to-completion steps (RTC). Cycle-based state machines can be explicitly told to perform an RTC step, while event-driven state machines run an RTC step implicitly when an incoming event is raised.
The
proceed statement consists of the keyword
proceed
and an indication by what to proceed. Two variants are available:
proceed
number
time_unit – This variant instructs the state machine to proceed by the specified time, e.g.,
proceed 30 s
proceeds by 30 seconds. For cycle-based statecharts, RTC steps will be performed based on the defined cycle time. For example,
proceed 400 ms
on a statechart with
@CycleBased(200) annotation, two RTC steps will be performed. Supported time units are:
s
– seconds
ms
– milliseconds
us
– microseconds
ns
– nanoseconds
proceed
number
cycle
– This variant is only available for cycle-based statemachines and instructs the state machine to perform
number run-to-completion steps. Proceeding cycles also implies proceeding of time based on the cycle period defined by the
@CycleBased() annotation.
Example for a machine with @CycleBased(200) annotation:
raise operate
proceed 100 ms
raise user.click
proceed 1 cycle
The statement
proceed 100 ms
will proceed the time accordingly, however, as the cycle period is set to 200 ms, no RTC step is performed. Hence, the event
operate is not yet handled. The subsequent statement
proceed 1 cycle
will proceed the time by exactly 100 ms so that an RTC step is executed. In this RTC step, both events are active simultaneously. For an event-driven machine the
proceed 1 cycle statement is not available. An RTC step is performed for each
raise statement instead.
When running an SCTUnit test, the state machine does not run in real time, but in
virtual time instead. That is, a statement like
proceed 3600 s
does not have to wait for one hour of real time to elapse. Instead the state machine “leaps” by one hour in an instant, raises all affected time events, and processes them.
Figure "Proceed statement grammar" summarizes the structure of the proceed statement:
Proceed statement grammar
Variables and constants (for brevity we’ll summarize both as “variables”) can be defined as specified in the statechart language, please see sections "Variables" and "Constants" for all the details.
However, variables in the SCTUnit language must always be initialized. For example, while a definition like
var sum: integer
is fine in the statechart language, it is an error in the SCTUnit language. You would rather have to write something like
var sum: integer = 0
Variables can be defined in the scope of the test class or in operations.
The if statement executes a sequence of statements depending on a condition.
Example 1:
if (i < 5) {
raise do_cycle
}
The do_cycle event is raised if the variable i has a value that is less than 5. Otherwise nothing happens.
Example 2:
if (i < 5) {
raise do_cycle
} else {
raise button5
}
The do_cycle event is raised if the variable i has a value that is less than 5. If i is equal to or greater than 5 the button5 event is raised instead.
The
if statement starts with the keyword
if
, followed by a boolean expression in parenthesis, followed by a sequence of statements in braces. The sequence of statements must contain at least one statement. It is executed if and only if the boolean expression evaluates to
true.
And optional
else clause may follow. It consists of the keyword
else
, followed by a sequence of statements in braces. The sequence of statements must contain at least one statement. It is executed if and only if the boolean expression evaluates to
false.
Figure "If statement grammar" summarizes the structure of the if statement:
If statement grammar
The while statement executes a sequence of statements repeatedly, as long as a condition is fulfilled.
Example:
var i : integer = 0
while (i < 10) {
raise do_cycle
proceed 1 cycle
i = i + 1
}
The statements in the while loop’s body are executed ten times.
The
while statement starts with the keyword
while
, followed by a boolean expression in parenthesis, followed by a sequence of statements in braces, the loop’s body. The loop’s body must contain at least one statement. It is executed repeatedly if and only if the boolean expression evaluates to
true. The expression is evaluated before the first execution of the loop body and after each execution of the loop body.
Figure "While statement grammar" summarizes the structure of the while statement:
While statement grammar
The keywords
active
,
is_active
and
is_final
make it possible to retrieve certain aspects of the state machine’s status as boolean values and e.g., use them in
assertions.
Example:
assert is_active
The assertion succeeds if at least one state is active. This is always the case if the state machine has been entered and has not been exited. Please note that a final state can be active, too.
The assertion fails if the state machine has not been entered or has been exited.
Example:
assert is_final
The assertion succeeds if the state machine is active (see above) and all its active states are final states.
The assertion fails if the state machine is not active (see above) or it has at least one active state that is not a final state.
Example:
assert active(main_region.StateA)
The assertion succeeds if the specified state is active. States have to be fully qualified with their containing region etc., because state names are not unique in a statechart.
The assertion fails if the specified state is not active.
The mock statement allows you to mock operations defined in the statechart. You can specify what should be returned when the operation is called, even depending on the given input parameters.
Consider a complex operation getNeutronFlux, which takes a real value as an argument and returns a real value as a result. During semantic unit testing of your statechart – as opposed to integration testing –, you won’t want to integrate the operation into your testing environment. Aside from that, it’s currently not possible to call operations from SCTUnit, for example, operations defined in a Java class.
Your statechart, however, depends on getNeutronFlux returning actual results, as can be seen in the simple model shown in figure "Neutron flux statechart":
Neutron flux statechart
While State_A is active, the state machine will call getNeutronFlux(p) during each run-to-completion step and, depending on the results, will take the transition to State_B (or not).
However, how could you test the behaviour of your statechart if you cannot call an actual operation and retrieve its results?
That’s what the mock statement is for. It is a makeshift that mimics an actual operation call by two mechanisms:
A test can create an arbitrary number of such mappings, i.e., it can use the mock statement multiple times for multiple operations, or for multiple parameter list of the same operation.
Consider the following test:
@Test
operation testNeutronFlux() {
mock getNeutronFlux(12.0) returns (1000000000.0)
mock getNeutronFlux(18.0) returns (4.3)
enter
p = 18.0
proceed 1 cycle
assert active(neutronFlux.main_region.State_A)
p = 12.0
proceed 1 cycle
assert active(neutronFlux.main_region.State_B)
}
The effect of the two mock statements is as follows:
The test assigns 18.0 to the statechart variable
p, and performs one run-to-completion step. During this RTC, the state machine calls
getNeutronFlux(p) in order to evaluate the guard condition
[getNeutronFlux(p) >= 100.0]
. Since
p has a value of 18.0, the mocked operation returns 4.3, and the transition from
State_A to
State_B is not taken.
After that, the test sets p to 12.0 and executes another RTC. This time, the mocked operation returns 1000000000.0, the guard condition evaluates to true and the state machine transitions to State_B.
Calling getNeutronFlux with any other parameter value than 12.0 or 18.0 would not only let the test fail, but would also throw an exception, because the actual operation cannot be called in test mode (simulation mode) and its return value is undefined.
In order to avoid an exception to be thrown, you can define a mock statement with a default return value. This value will be returned from all calls of the mocked operation that are not explicitly overridden by mock statements with specific parameters.
The mock statements in the example above might have better been written as follows:
mock getNeutronFlux returns (-1.0)
mock getNeutronFlux(12.0) returns (1000000000.0)
mock getNeutronFlux(18.0) returns (4.3)
The first mock statements defines a return value of -1.0 for each and every call to the getNeutronFlux operation, irrespective of the parameter value. The following statements, however, override this setting for the parameter values 12.0 and 18.0.
Please note: The order of the mock statements is important! You should define the general case first, followed by specifying return values for specific parameter lists.
The type of the default value specified in the mock statement must match the return type of the mocked operation.
Figure "Mock statement grammar" summarizes the structure of the mock statement:
Mock statement grammar
A
test suite aggregates a set of
test classes into a logical unit. It is contained in a file with a
.sctunit filename extension.
Example:
testsuite MyTestSuite {
TestClassA,
TestClassB,
TestClassC
}
The test suite MyTest_Suite comprises the test classes TestClassA, TestClassB, and TestClassC.
The nice thing about test suites is that you can run all tests of all the test classes at once. Right-click on the test suite file, say, mytestsuite.sctunit, and select Run As → SCTUnit in the context menu. All tests in all test classes referenced in the test suite will be executed.
You can put test classes that are testing different statecharts into a single test suite. However, all test classes within a test suite must pertain to statecharts using the same language domain. For example, if you have a statechart using itemis CREATE' default domain and another statechart using the C domain, you cannot put their respective test classes into the same test suite. Instead, you would have to write two different test suites: one for the test classes testing your “normal” statecharts, the another one for testing your “C” statecharts.
The
header of a test suite consists of the keyword
testsuite
, followed by the name of the test suite.
The
body of a test suite is enclosed in braces ({ … }
). It consists of one or more names of test classes, separated by comma.
Figure "Test suite grammar" summarizes the structure of a test suite:
Test suite grammar
Test suites are namespace-aware: Using the package statement , you can insert a test suite into a specific namespace.
You can organise your test classes and test suites in different namespaces. Each test class or test suite can assign itself to a namespace by the package statement.
Use the package statement to determine a namespace for the test class or test suite in the current .sctunit file. The package statement is optional, but if you use it, it must be the first statement of your .sctunit file. Test classes and test suites without a preceeding package statement will be put into the default namespace.
Example:
package foo.light_switch.test
testclass light_switch_tests for statechart light_switch {
…
}
The package statement puts the light_switch_tests test class into the foo.light_switch.test namespace.
The
package statement consists of the keyword
package
, followed by the package name (namespace). The package name is fully-qualified, i.e., in dot notation.
Please see section "Test units" for a summary of the package statement’s and related statements' grammar.
A test unit is either a test class or a test suite. It is contained in a file with a .sctunit filename extension.