Basics

Guides

API Reference

Menu

Basics

Guides

API Reference

Unit Testing in Aussom

Aussom provides a built-in unit testing framework through the aunit module. The framework supports test class and method annotations, lifecycle hooks, a rich set of assertion helpers, and mocking and spying capabilities. Tests can be run from the command line or programmatically using the testRunner class.


Getting Started

To use the unit testing framework, include the aunit module at the top of your test file.

include aunit;

This makes the test static class and the testRunner class available.


Defining a Test Class

A test class is any class annotated with @Test. The name parameter provides a human-readable label for the test suite.

@Test(name = "Calculator Tests")
class calculatorTests {
    // test methods go here
}

Lifecycle Annotations

Two annotations control setup and teardown logic that run around each test.

@Before — marks a method to run before each test function in the class. Use this to initialise shared state or resources.

@After — marks a method to run after each test function in the class. Use this for cleanup.

@Test(name = "Calculator Tests")
class calculatorTests {
    @Before
    public setUp() {
        c.info("Setting up before each test.");
    }

    @After
    public tearDown() {
        c.info("Cleaning up after each test.");
    }
}

Only one @Before and one @After function may be defined per test class.


Writing Test Functions

Individual test methods are annotated with @Test(name = "..."). Each test method should return the result of a test assertion call, or throw an exception on failure.

@Test(name = "Calculator Tests")
class calculatorTests {
    @Test(name = "Addition returns correct result.")
    public testAddition() {
        result = 2 + 3;
        return test.expect(result, 5);
    }

    @Test(name = "Division returns a double.")
    public testDivision() {
        result = 10 / 4.0;
        return test.expectDouble(result);
    }
}

The test Assertion Class

The test static class provides a set of assertion helpers. Each function returns true on success and throws an exception if the assertion fails — which is how the test runner detects a failure.

Equality

  • test.expect(Item, ToBe) — asserts that Item equals ToBe.
test.expect(2 + 2, 4);
test.expect(name, "Tyler Durden");

Null Checks

  • test.expectNull(Item) — asserts that Item is null.
  • test.expectNotNull(Item) — asserts that Item is not null.
test.expectNull(uninitialised);
test.expectNotNull(new user());

Type Checks

  • test.expectString(Item) — asserts Item is a string.
  • test.expectBool(Item) — asserts Item is a bool.
  • test.expectInt(Item) — asserts Item is an int.
  • test.expectDouble(Item) — asserts Item is a double.
  • test.expectNumber(Item) — asserts Item is either an int or a double.
  • test.expectList(Item) — asserts Item is a list.
  • test.expectMap(Item) — asserts Item is a map.
  • test.expectCallback(Item) — asserts Item is a callback.
  • test.expectObject(Item, ClassName) — asserts Item is an instance of the named class.
test.expectString("hello");
test.expectInt(42);
test.expectList([1, 2, 3]);
test.expectObject(u, "user");

The @Mock Annotation

The @Mock annotation can be placed on a class member declaration to mark it as a mock placeholder. The member is initialised to null, ready to be set or mocked by individual test methods.

@Test(name = "My Tests")
class myTests {
    @Mock
    public service;

    @Test(name = "Service is null before setup.")
    public testServiceIsNull() {
        return test.expectNull(this.service);
    }
}

Mocking and Spying

Aussom supports mocking and spying on any object's functions directly, without a separate mocking library. These capabilities are available on all object instances.

Simple Mock

obj.mock(FunctionName, ReturnValue) — replaces the named function on the object so it always returns the provided value, regardless of how it is called.

u = new user();
u.firstName = "Austin";

// Override getName() to always return "Avery".
u.mock("getName", "Avery");

test.expect(u.getName(), "Avery");

Conditional Mock (mockWhen)

obj.mockWhen(FunctionName, Condition, ReturnValue) — replaces the function only when the provided callback condition returns true. When the condition returns false, the real function runs normally.

The condition callback receives two arguments: the object instance (cobj) and the argument list that was passed to the function (args).

u = new user();
u.firstName = "Tyler";
u.lastName = "Durden";

// Only return "Bob Durden" when firstName is "Tyler".
u.mockWhen("getName", ::checkFirstName, "Bob Durden");

test.expect(u.getName(), "Bob Durden");

// Change the first name so the condition no longer matches.
u.firstName = "Fred";
test.expect(u.getName(), "Fred Durden");

// ...

public checkFirstName(cobj, args) {
    if (cobj.firstName == "Tyler") { return true; }
    return false;
}

Spying

obj.setSpy(FunctionName) — enables call recording on the named function. Every invocation is recorded. The real function still runs normally.

obj.getSpy(FunctionName) — returns the list of recorded spy entries. The # operator gives the list length.

u = new user();
u.setName("Tyler", "Durden");

// Enable spy recording on getName.
u.setSpy("getName");

u.getName();
test.expect(#u.getSpy("getName"), 1);

u.getName();
test.expect(#u.getSpy("getName"), 2);

Running Tests

There are two ways to run tests: using test.runTestsForClass() for a quick single-class run, or using the testRunner class for programmatic control.

test.runTestsForClass(ClassName)

Runs all @Test-annotated methods in the named class and logs the results to standard output. This approach requires the test.aussom.runner security manager property to be set to true.

res = test.runTestsForClass("calculatorTests");
c.info(res);

The testRunner Class

testRunner gives you full control: load test files or inline code, introspect available classes and functions, run lifecycle hooks individually, and execute specific tests. This is the recommended approach for embedding tests inside an application or for building a custom test harness.

Loading Tests

runner = new testRunner();

// Load from a .aus file on disk.
runner.loadTestFile("tests/calculatorTests.aus");

// Or load from an inline string (useful for dynamic testing).
runner.loadTestString("calcTests.aus", ausCode);

Introspecting the Loaded Tests

// List all test classes found in the loaded files.
classes = runner.getTestClasses();
c.info(classes);

// List all @Test functions for a specific class.
functions = runner.getTestFunctions("calculatorTests");
c.info(functions);

// Check whether lifecycle hooks are defined.
c.info(runner.hasBefore("calculatorTests"));
c.info(runner.hasAfter("calculatorTests"));

Running Tests

// Run the @Before lifecycle hook.
runner.runBefore("calculatorTests");

// Run a specific test function.
try {
    result = runner.runTest("calculatorTests", "testAddition");
    c.info("testAddition: passed");
} catch (e) {
    c.info("testAddition: failed - " + e);
}

// Run the @After lifecycle hook.
runner.runAfter("calculatorTests");

Clearing the Object Cache

testRunner caches one instance per class to keep state consistent across functions. Call clearClassObjectCache() to reset this between suites if needed.

runner.clearClassObjectCache();

Complete Example

Below is a self-contained test file that demonstrates the main features of the framework.

include aunit;

// The class under test.
class calculator {
    public add(int A, int B) { return A + B; }
    public divide(double A, double B) { return A / B; }
    public label() { return "Calculator v1"; }
}

// The test suite.
@Test(name = "Calculator Tests")
class calculatorTests {

    private calc = null;

    @Before
    public setUp() {
        this.calc = new calculator();
    }

    @After
    public tearDown() {
        this.calc = null;
    }

    @Test(name = "add() returns correct integer sum.")
    public testAdd() {
        return test.expect(this.calc.add(2, 3), 5);
    }

    @Test(name = "divide() returns a double.")
    public testDivide() {
        return test.expectDouble(this.calc.divide(10.0, 4.0));
    }

    @Test(name = "label() returns a string.")
    public testLabel() {
        return test.expectString(this.calc.label());
    }

    @Test(name = "mock() overrides label().")
    public testMockLabel() {
        this.calc.mock("label", "Mocked!");
        return test.expect(this.calc.label(), "Mocked!");
    }
}

// Runner entry point.
class runTests {
    public main(args) {
        runner = new testRunner();
        runner.loadTestFile("calculatorTests.aus");

        classes = runner.getTestClasses();
        for (cls : classes) {
            c.info("Running suite: " + cls);
            runner.runBefore(cls);
            fns = runner.getTestFunctions(cls);
            for (fn : fns) {
                try {
                    runner.runTest(cls, fn);
                    c.info("  PASS: " + fn);
                } catch (e) {
                    c.info("  FAIL: " + fn + " - " + e);
                }
            }
            runner.runAfter(cls);
        }
    }
}