Unit testing with AUnit

Unit tests play a crucial role in tightening the developer feedback loop. They enable developers to receive fast feedback on the validity of their application’s behaviour, and can assist with development processes such as TDD. Whilst not replacing the need for system tests, unit tests contribute to the suite of testing methods available, helping developers create elegant, reliable code.

AUnit is a unit test and packaging tool for Apama. Unit tests are written directly in EPL, enabling developers to introspect and assert their application at an event object level.

TestEvent File

All unit tests are written using a TestEvent file. A TestEvent file is any *.mon file whose event definition is adequately annotated using prescribed AUnit keywords. Once the TestEvent has been declared, any number of its actions can be flagged as a ‘ test ‘. Using the com.aunit.Asserter object, developers can perform asserts on any primitive type, as well as on event objects using the .toString() method.

//@Test
action realNumbersTest() {
    float result := math.add([-10.0,100.0,3.0,5.0,2.0]);
    asserter.assertFloat("Real Numbers Test", result, 100.0);
}

When an action is preceded by a //@Test annotation, AUnit treats the action as a test. Test actions must perform at least one assertion, and are classified as either synchronous or asynchronous depending on their signature.

Synchronous tests are those whose test action takes no parameters, similar to the realNumbersTest example above. These are the simplest type of test, and should be used where possible. Sometimes however, developers need to test the response of a service or asynchronous interface. In such cases, asynchronous tests are well suited.

//@Test
action timeTest(action<> cbDone) {
    float ct := currentTime;
 
    on wait(1.0) {
        if ct < currentTime then {
            asserter.assertTrue("Time moves forwards!", true);
        } else {
            asserter.assertTrue("Time stands still", false);
        }
 
        cbDone();
    }
}

As seen in the example above, asynchronous test actions have a single callback parameter of type action<> which is provided by the test framework. Once a test action has completed all its assertions, it calls back to the AUnit framework by calling cbDone() . Users should take care when writing asynchronous tests as failing to call the cbDone() action will cause the test framework to hang. Therefore all possible paths (both successes and failures) should result in the cbDone() being called.

In addition to the Test annotation, users can specify one Setup, Teardown and Initialise action per TestEvent . Those familiar with other unit test frameworks will recognise the setup and teardown actions which provide a mechanism for state to be setup and cleared between test action runs, and the initialise action which is called one time per TestEvent (at startup).

Whilst AUnit supports any number of test actions to exist within a TestEvent , it is best practice to have a new TestEvent file for each component being tested, logically grouping related tests. If helper events are required to perform the testing, they too can be defined in the TestEvent file. The example below demonstrates a SequenceMath event and its supporting tests in the same file. Whilst it is considered best practice to separate production code from test code, this approach has been used to simplify the example.

Complete Sample Test File

Suppose a user has a helper event which performs the addition operation on a sequence of floats. The user may wish to ensure the return values given a number of different scenarios:

  • Real Numbers
  • NaN values
  • Blank Sequence

To achieve this, a TestEvent can be defined, with each of the above scenarios supported by a corresponding test action.

// Helper Event to perform operations on sequence of floats
 
package com.aunit.samples;
 
event SequenceMath {
 
    action add(sequence<float> numbers) returns float {
 
        float total := 0.0;
 
        float i;
        for i in numbers {
            total := total + i;
        }
 
        return total;
    }
}
 
//@Depends UnitTest
event SequenceMathTest {
 
    com.aunit.Asserter asserter;
    SequenceMath math;
 
    //@Test
    action realNumbersTest() {
        float result := math.add([-10.0,100.0,3.0,5.0,2.0]);
        asserter.assertFloat("Real Numbers Test", result, 100.0);
    }
 
    //@Test
    action blankSequenceTest() {
        float result := math.add(new sequence<float>);
        asserter.assertFloat("Blank Sequence Test", result, 0.0);
    }
 
    //@Test
    action nanValueTest() {
        float result := math.add([-10.0 ,float.NAN,3.0,float.NAN,2.0]);
        asserter.assertFloat("NaN Test", result, float.NAN);
    }
 
    //@Setup
    action setup(action<> cbSetup) {
        math := new SequenceMath; // used to clear any state of event being tested
        cbSetup();
    }
 
    //@Teardown
    action teardown(action<> cbTeardown) {
        cbTeardown(); // nothing to teardown
    }
 
    //@Initialise
    action initialise(action<> cbInit) {
        cbInit(); // nothing to initialise
    }
}

After saving the above snippet to $AUNIT_HOME\workspace\Samples\SequenceMath.mon , the user can run the tests by running the following command in the Apama Command Prompt:

>> aunit test Samples

2016-10-21 23:26:07,051 INFO ==============================================================

2016-10-21 23:26:07,052 INFO Id : SequenceMathTest

2016-10-21 23:26:07,053 INFO Title: SequenceMathTest

2016-10-21 23:26:07,055 INFO ==============================================================

2016-10-21 23:26:08,740 INFO

2016-10-21 23:26:08,742 INFO cleanup:

2016-10-21 23:26:08,798 INFO Executed shutdown , exit status 0

2016-10-21 23:26:08,848 INFO

2016-10-21 23:26:08,851 INFO Test duration: 1.80 secs

2016-10-21 23:26:08,851 INFO Test final outcome: PASSED

2016-10-21 23:26:08,852 INFO

2016-10-21 23:26:08,878 CRIT

2016-10-21 23:26:08,880 CRIT Test duration: 1.83 (secs)

2016-10-21 23:26:08,881 CRIT

2016-10-21 23:26:08,881 CRIT Summary of non passes:

2016-10-21 23:26:08,881 CRIT THERE WERE NO NON PASSES

Upon running ‘ aunit test ‘, all TestEvents are converted to a pysys test and run. Each TestEvent receives an isolated correlator environment with only its dependencies injected. The output above is a sample of how a successful test run should look. Test failure details are provided in the pysys Output folder similar to when systems testing.

The addition of unit tests to your Apama application can help with testing expected return values from functions, right through to complex boundary cases. Whilst not replacing the need for systems testing, it is another tool in the developers artillery to developing fast, stable applications on the Apama platform.

To download AUnit and for more information, including further examples as well as details on the inner workings of AUnit please refer to the readme: GitHub - antoinewaugh/aunit: Unit Testing Framework for Apama

For further information on Apama systems testing using pysys please visit How to test Apama Applications.