The SCTUnit language

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 YAKINDU Statechart Tools. The editor will show you all possible input choices that are valid at your cursor location.

Test classes

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 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 and tests

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

Operation grammar

Scopes

An operation can define its own variables. Aside from these

  • local variables

it also has access to

  • variables and operations defined in the test class,
  • variables, operations, events, and states defined in an interface of the statechart controlled by the operation’s test class,
  • entities imported by the statechart controlled by the operation’s test class.

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.

Annotations

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.

Statements and expressions

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.

Returning an operation’s result

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

Return statement grammar

Assertions

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 and assert 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.

Asserting an operation call

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.

Asserting an outgoing event

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

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
    }
}

Assertion grammar

Figure "Assertion grammar" summarizes the structure of an assertion:


Assertion grammar

Assertion grammar

Entering a state machine

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 .

Exiting a state machine

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"

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)
    }
}

Raising an event

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".

Proceeding a state machine

The tested state machine does not execute any run-to-completion steps (RTC) by itself, but only if being told so by way of the proceed statement. The latter can advance the state machine by a specified number of RTCs or by a certain period of time. Event-driven state machines run an RTC when an incoming event is raised. The syntax proceed _n_ cycle is not available for these state machines.

Example:

raise operate
raise user.click
proceed 1 cycle

This example raises the operate and user.click events. The subsequent statement proceed 1 cycle causes the state machine to perform one run-to-completion step, potentially acting on the raised events.

The proceed statement consists of the keyword proceed and an indication by what to proceed. Two variants are available:

  • proceed number cycle – This variant instructs the state machine to perform number run-to-completion steps. Not available for event-driven state machines.
  • 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. Supported time units are:
    • s – seconds
    • ms – milliseconds
    • us – microseconds
    • ns – nanoseconds

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

Proceed statement grammar

Defining variables and constants

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.


Conditional statements

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

If statement grammar


Loops

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

While statement grammar


Retrieving the state machine’s status

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.


Mocking an operation call

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

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:

  1. It creates a static mapping from a specific operation call with a specific list of parameter values to a specific return value.
  2. If that operation is called with exactly that specific list of parameter values the caller receives the mapped return value as a result.

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:

  • If getNeutronFlux(12.0) is called, the return value is 1000000000.0.
  • If getNeutronFlux(18.0) is called, the return value is 4.3.
  • If getNeutronFlux is called with any other parameter value, the return value is undefined.

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

Mock statement grammar

Test suites


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 YAKINDU Statechart Tools' 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 suite grammar

Test suites are namespace-aware: Using the package statement , you can insert a test suite into a specific namespace.

Namespaces

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.

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.

Test units

A test unit is either a test class or a test suite. It is contained in a file with a .sctunit filename extension.