Test-driven statechart development with SCTUnit

Brief introduction to test-driven development

In test-driven development, before writing any code the developer creates a unit test that checks the to-be-established implementation for correctness. Only after that, the developer codes the implementation and executes the test. If the test fails, the developer has to fix the implementation – perhaps repeatedly – until the test succeeds.

Tests are not written for the complete software product in advance, but rather on a per-feature basis. That is, the developer writes tests only for the one single feature he is going to implement next. Having successfully finished that implementation, the developer should refactor his code and his tests. The most recent additions might have opened opportunities to restructure one or the other, thus reducing complexity and/or adding readability. Refactoring must never change the semantics of the code. To ensure that, tests come in handy and should be run after each refactoring step.

Test-driven statechart modelling

In model-driven software development (MDSD), specific parts of the software are created by a code generator instead of being written manually. This also pertains to code generated from statechart models.

The nice thing about generated code is that we can safely assume it to be correct – at least as long as the code generator developers have tested their product thoroughly enough. However, what about the model? While the generated code might be correct, this won’t help at all if the underlying model is broken in the first place.

SCTUnit is a scripting and testing framework for writing unit tests for statechart models. You can validate the behaviour of your statechart by running these tests. Each test, written in the SCTUnit language, consists of a well-defined sequence of instructions. When running a test, these instructions are applied to a state machine under control and possibly changes that state machine in a specific way. You put down the expected effects of these changes as assertions. An assertion is used to check whether a specific condition is fulfilled, for example certain states being active or not, variables having specific values, operations having been executed, etc. SCTUnit allows test-driven development of statechart models on the semantic level.

The subsequent sections will first introduce SCTUnit by example and then explain the SCTUnit language in full.

SCTUnit by example

This section introduces SCTUnit by a simple statechart example that models a light switch. We will develop this statechart step by step using a test-driven approach. That is, for each single development step, we will first write a test, then change our statechart model, then run all the tests we created so far until they succeed. After that, we will proceed with the next development step.

Light switch requirements

Before implementing any tests or models, let’s write down the requirements for our light switch statechart model:

  1. When the state machine starts, the model is in the Off state (initially the light is off).
  2. If the Off state is active and the operate event occurs (a user operates the light switch), the state machine changes from the Off state to the On state (the light is switched on).
  3. If the On state is active and the operate event occurs (a user operates the light switch), the state machine changes from the On state to the Off state (the light is switched off).

Specifying the initial behaviour as a test

Now we need an Eclipse project to host our statechart model. So let’s create one, naming it light_switch. Within the project, we create two folders, model and test, for – you guessed it – the model and the tests, respectively.

In the test directory, proceed as follows to create a SCTUnit test file:

  1. In the project explorer view, right-click on the test folder. The context menu opens.
  2. In the context menu, select New → Other…. The New wizard opens.
  3. In the wizard, select YAKINDU SCT → SCTUnit test case.
  4. Click Next >. The wizard switches to the New SCTUnit Test Class page.
  5. In the Filename text field, specify the name of the file to contain the SCTUnit tests, say, light_switch.sctunit. The filename extension must always be .sctunit.
  6. Click Finish.
  7. If a dialog appears asking whether you want to add the Xtext nature to the project, please click Yes.

As a result, the empty file light_switch.sctunit is created in the test folder and is opened in an editor.

In the spirit of test-driven development, create a test for the light switch model’s initial behaviour by writing the following in the light_switch.sctunit file:

testclass light_switch_tests for statechart light_switch {

    @Test
    operation isStateOffActiveAfterInit () {
        enter
        assert active (light_switch.main_region.Off)
    }

}

Let’s walk through this example line by line and see what all of this means.

  • testclass light_switch_tests for statechart light_switch {
    • The keyword testclass introduces a test class, a collection of tests. In this example, it is named light_switch_tests.
    • A test class always refers to a statechart that is under control and is to be tested. This relationship is established by the for statechart clause. The keywords for statechart are followed by the name of the statechart, here: light_switch. However, since there is no light_switch statechart yet, an error marker is attached to the statechart name, as shown in the following screenshot:
      Initial test
    • The body of the test class is bracketed in braces, i.e. between { and }.
  • @Test
    operation isStateOffActiveAfterInit () {
    • Each test is an operation that is annotated by @Test. This example defines the operation isStateOffActiveAfterInit. It has no parameters, which is indicated by (). As we will see later, an operation does not necessarily have to be a test, but can be a mere subroutine. Such operations can have parameters. The operation’s body is bracketed in braces.
  • enter
    • The enter statement initializes and starts the state machine; it „enters” it. As a consequence, the state machine activates the state that is pointed to by the initial state.
  • assert active (light_switch.main_region.Off)
    • The assert statement expects that a specified condition is fulfilled. Using the built-in active function, our example’s assertion expects that the state Off is active. The state is addressed in a fully-qualified manner, i.e. statechart. region. state. However, since we don’t have a statechart model yet and less than ever any state, the Off state in the assertion is flagged as an error.

Implementing a minimal light switch model

Since the test has error markers, we can say that it fails even without being run. So let’s fix that with minimal effort and create a model that is called light_switch and has a state Off. Figure "First step of creating a light switch model" shows the resulting model.

First step of creating a light switch model

First step of creating a light switch model

Running the test

Figure "First step of creating a light switch model" also shows that the error markers of the test class have been cleared. This is no surprise since our minimal statechart model has the name that is specified in the test and it also contains the Off state.

Now we can execute the test class. In order to do so, proceed as follows:

  • Right-click on the light_switch.sctunit file in the test directory. The context menu opens.
  • In the context menu, select Run As → SCTUnit
    Run as SCTUnit
  • The test in the test class is executed.
  • The results are displayed in the JUnit tab. A green bar is shown, which means our model fulfills the expectations expressed in the test class.
    SCTUnit test succeeded

Adding more tests

Writing a test for the second requirement

The model by now implements the first requirement specified in section "Light switch requirements".

So let’s care for the second requirement, formulate it as a SCTUnit test, and add it to the test class.

The whole test class now looks like this:

testclass light_switch_tests for statechart light_switch {

    @Test
    operation isStateOffActiveAfterInit () {
        enter
        assert active (light_switch.main_region.Off)
    }

    @Test
    operation isStateOnActiveAfterOperateInOffState () {
        enter
        raise operate
        proceed 1 cycle
        assert active (light_switch.main_region.On)
    }
}

The raise operate instruction raises the operate event. However, aside from this event having been raised, nothing will happen yet. The next instruction will change that.

The proceed 1 cycle instruction orders the state machine to execute one run-to-completion step (RTC). During this RTC, the state machine can react to the raised operate event.

Processing the operate event should cause the On state to become active. This is checked by the following assertion:
assert active (light_switch.main_region.On)

To make the test class syntactically correct, the statechart model must be amended by the operate event and by the On state.

Detecting a semantically wrong implementation

However, in order to demonstrate what happens if an SCTUnit test fails, let’s have a look at a wrong implementation, as shown in figure "Erroneous light switch model". Here the operate event does not trigger a transition from Off to On but from Off back to Off.

Erroneous light switch model

Erroneous light switch model

The test operation isStateOnActiveAfterOperateInOffState should detect this semantic error and alert us. Running the amended test class as an SCTUnit again indeed leads to the following result:

SCTUnit test failed

The large bar is red now instead of green, meaning that at least one test failed. Below the bar the test class and its tests are listed and it is indicated which tests succeeded and which tests failed. We can see that our isStateOffActiveAfterInit test still succeeds; so while we didn’t correctly implement the second requirement, we at least didn’t break the first one.

Fixing the model as follows turns the test green:

Fixed light switch model

Calling operations

Okay, so let’s write a test for the third and final requirement:

    @Test
    operation isStateOffActiveAfterOperateInOnState () {
        isStateOnActiveAfterOperateInOffState
        raise operate
        proceed 1 cycle
        assert active (light_switch.main_region.Off)
    }

A prerequisite for dealing with an operate event in the On state is to get into that state in the first place. However, we have implemented the necessary actions before, namely in the test isStateOnActiveAfterOperateInOffState. The great thing is that we can just use that test by calling it as subroutine. This is like calling a function or method in any other programming language.

We know that after executing isStateOnActiveAfterOperateInOffState the On state is active – we even assert that in the called test operation. So now we just have to raise operate again, run an RTC and check whether we are in the Off state after that.

Looping

By going from Off to On and back to Off, we have ensured now that the light switch model behaves as specified. However, we might want to check whether this is still the case if we go through this cycle a number of times, say twice or 20,000 times. To support scenarios like this, the SCTUnit language provides a couple of advanced features, see section The SCTUnit language. Here we use the while loop to iterate over the off-on-off cycle a couple of times:

    @Test
    operation isStateOffActiveAfter10Cycles () {
        enter
        var i: integer = 0
        while (i < 10) {
            raise operate
            proceed 1 cycle
            assert active (light_switch.main_region.On)
            raise operate
            proceed 1 cycle
            assert active (light_switch.main_region.Off)
            i = i + 1
        }
    }

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. More precisely, since the SCTUnit language is an extension of the statechart language, you will find herein a description of those 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 the introduction of 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 an optional annotation, typically @Test, followed by 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.

The body of an operation is enclosed in braces ({ … }). It consists of a sequence of statements, which may be empty.

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 on the test class level,
  • 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 executed during a test. If you need them to be called you can mock them.

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.

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

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 with (42, 815)

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 first variant of the assert called statement begins with the keywords assert called and is followed by the name of (resp. reference to) the operation to check. This variant of the assert called statement checks whether the named operation has been called during the execution of this test, irrespective of its parameters.

In the second variant of the assert called statement the keyword with follows the structure described above. It is followed by a parenthesized list of parameters. This variant of the assert called statement not only checks whether the named operation has been called, but for the test not to fail it also requires that the operation has been called with the specified parameters.

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 .

The enter statement’s grammar is very simple, as figure "Enter grammar" shows:

Enter grammar

Enter grammar

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

The exit statement’s grammar is very simple, as figure "Exit grammar" shows:

Exit grammar

Exit grammar

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 in the state machine’s default interface.

Example 2:

raise user.click

This statement raises the click event in the state machine’s user interface.

Since state machine internals are inaccessible to SCTUnit tests and outgoing events are, well, outgoing, only incoming events can be raised. Events in the internal scope or outgoing events defined in an interface 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.

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.
  • 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’s concept of „time” is decoupled from real time. 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

Figure "Variable definition grammar" summarizes the structure of a variable definition:

Variable definition grammar

Variable definition grammar


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 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 or composite 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 or composite state.


Mocking an operation call

The mock statement acts as a placeholder for an operation call and its return value.

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, SCTUnit tests are executed in YAKINDU Statechart Tools' simulation mode, where you cannot call operations anyway.

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

Returning an operation’s result

The return statement terminates the execution of the current operation and returns the value of an expression to the caller. The type of the expression must match the operation’s return type.

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

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.

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' standard 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.

Generating SCTUnit source code

Executing SCTUnit tests within the Eclipse development environment as described in section "Running the test" is a nice and handy feature the developers. However, unit tests are usually also required to be executed automatically in continuous integration (CI) environments, i.e. outside of and independent from Eclipse or any other IDE. Additionally, it should generally be possible to inspect what exactly happens inside a unit test, in order to be able to verify that what it does when executed in the right thing. This is of particular importance for software that is to be deployed in safety-critical areas.

Generating unit tests as source code comes as a solution. Source code can be inspected, compiled, and integrated wherever needed.

This section explains how you can generate your SCTUnit tests as source code with YAKINDU Statechart Tools. Supported programming languages are Java, C, and C++. The generated Java classes use JUnit as their testing framework and Mockito for mocked methods. C and C++ sources rest upon the Google Test unit testing library.

In order to use the SCTUnit code generators effectively, you should already be familiar with generating source code from statecharts, as explained in chapter "Generating state machine code". In fact, you have to generate your statechart as source code first, so that the SCTUnit tests have something to be executed upon. Below we are assuming that you have your statechart code generator model and the generated code handy. Generating SCTUnit test source code follows the same principcles as generating state machine source code, so we can focus on SCTUnit essentials here.

Creating a generation model

The first step is to create a generation model (SGen) for the SCTUnit test, similar to explanation in section "Generating state machine code".

  1. In the main menu, select File → New → Other…. The New dialog opens.
  2. In the New dialog, select YAKINDU SCT → Code Generator Model.
  3. Click on Next. The YAKINDU Generator Model wizard opens.
  4. Select a project and a folder as the SGen model’s location. The filename must end on .sgen.
  5. Click on Next.
  6. In the Generator drop-down menu, select one of the source code generators whose names begin with „SCTUnit”, e.g. „SCTUnit Java Code Generator”. As a consequence, the Choose list below the drop-down menu switches from showing statechart models to displaying SCTUnit files.
  7. In the Choose list, check the SCTUnit files you want to generate as source code.
  8. Click on Finish.

As a result, the SGen model file is created. Figure "A sample SCTUnit SGen model" shows a generator model for the SCTUnit Java generator. The target language is reflected in the generator ID „sctunit::java”. The generator IDs for C and C++ are „sctunit::c” and „sctunit::cpp”, respectively. Each SCTUnit test unit is contained in a section marked with the test keyword, followed by the respective fully-qualified test unit name.

A sample SCTUnit SGen model

A sample SCTUnit SGen model

In this example, the generation model refers to the SCTUnit sample test class defined in section "The package statement" above. In that example, the test class is put into package foo.light_switch.test, while the test class itself is named light_switch_tests. Consequently, the test class' fully-qualified name is foo.light_switch.test.light_switch_tests, and that is what the generation model indicates in the test statement.

Features

A code generator’s behavior can be controlled by so-called features. SCTUnit code generators are supporting the same set of features the all statechart code generators support. The same applies to the features of the respective language-specific generators. The Outlet feature is of particular importance, since it determines where the generated artifacts will be created.

Documentation of feature details you can find here:

The SCTUnit Java code generator’s StatechartNaming feature

For Java, the generated state machine code on one hand and the SCUnit test classes on the other hand may well belong to different packages. However, in order to be able to execute the state machine, the SCTUnit tests need to know what the state machine’s package is.

This is what the StatechartNaming feature is good for. Let’s assume we added the following StatechartNaming feature to the generation model shown above:

feature StatechartNaming {
    basePackage = "bar.light_switch"
    implementationSuffix = ""
    libraryTargetFolder = "src-timer"
}

With these settings, the SCTunit code generator creates the test classes in the src-gen directory. Additionally, it creates a few timer classes. These classes provide the concept of „virtual” time and are used during test execution to simulate arbitrary long periods of time in an instant. The source code of these classes is generated in the directory denoted by the libraryTargetFolder property, here: src-timer. The timer classes belong to the bar.light_switch package. The test classes expect the timer classes and the state machine to reside in that very same package, in order to execute the state machine parameterized with the virtual timer.

The generated Java test class

The Java source code generated from SCTUnit test classes or test suites takes the form of JUnit tests and can be integrated in CI environments, for example.

In Eclipse, you can execute such a generated JUnit test by right-clicking on the corresponding .java file and selecting Run As → JUnit Test in the context menu.