Testing

Geblang ships a built-in test runner and an assertion framework in the test module. Tests are ordinary Geblang classes that extend test.Test and mark methods with the @test decorator. The CLI command geblang test discovers *_test.gb files, instantiates each test class, and runs every @test method.

A first test

import test;

class CalculatorTest extends test.Test {
    @test
    func adds(): void {
        this.assertEquals(4, 2 + 2);
    }

    @test
    func subtracts(): void {
        this.assertEquals(0, 2 - 2);
    }
}

Save as tests/calculator_test.gb and run:

geblang test tests/calculator_test.gb

You can pass either a single file or a directory. Directory paths are walked recursively for files matching *_test.gb.

Assertions

test.Test exposes a fluent set of assertion methods. Each raises an error with a descriptive message on failure; the runner counts the failure and continues to the next method.

Method Meaning
assertEquals(expected, actual) Deep-equal comparison
assertNotEquals(unexpected, actual) Inverse
assertTrue(value) value must be true
assertFalse(value) value must be false
assertNull(value) value must be null
assertNotNull(value) value must not be null
assertContains(container, needle) String/list/dict contains check
assertNotContains(container, needle) Inverse
assertEmpty(value) Empty string/list/dict/set/null
assertNotEmpty(value) Inverse
assertGreaterThan(threshold, actual) Numeric / string ordering
assertGreaterThanOrEqual(threshold, actual)
assertLessThan(threshold, actual)
assertLessThanOrEqual(threshold, actual)
fail([message]) Unconditional failure

Equality uses Geblang's value-equality rules: lists, dicts, sets, and class instances are compared field by field; enum variants compare on their name and payload.

Setup and teardown

test.Test recognises four optional lifecycle hooks. Override any of them on your subclass:

class DatabaseTest extends test.Test {
    int conn;

    func setupClass(): void {
        # runs once before any @test method on this class
    }

    func setup(): void {
        # runs before every @test method
    }

    func teardown(): void {
        # runs after every @test method, even if it failed
    }

    func teardownClass(): void {
        # runs once after the last @test method
    }

    @test
    func selectsRows(): void {
        # ...
    }
}

Tags and selective runs

Decorate @test methods with @tag("name") to put them into a category:

class WebTest extends test.Test {
    @tag("integration")
    @test
    func talksToAServer(): void {
        # ...
    }

    @test
    func parsesAUrlOffline(): void {
        # ...
    }
}

Then on the command line:

geblang test --tag integration tests/
geblang test --tag integration --tag slow tests/

geblang test runs only methods that carry at least one of the supplied tags. Without --tag it runs every @test method.

Verbose output

Pass --verbose (or -v) to print each test class and method as it runs, with PASS or FAIL per case. This is similar to PHPUnit's testdox format:

geblang test --verbose tests/
FunctoolsTest
  PASS pipeLeftToRight
  PASS pipeIdentityForEmpty
  PASS composeRightToLeft
  FAIL memoizeLruEvictsOldest: expected 4, got 3
tests: total=8 failed=1 passed=7

Without --verbose, only the failure lines and the summary are printed.

Capturing standard streams from a test

Tests that exercise code which writes to stdout, stderr, or reads from stdin can intercept those streams in-process. The capture helpers are evaluator-local; they do not touch the real terminal.

import io;
import test;

class GreetTest extends test.Test {
    @test
    func writesGreeting(): void {
        let capture = io.captureStdout();
        greet("Ada");
        let text = io.toString(capture);
        io.close(capture);
        this.assertTrue(text.contains("Hello, Ada"));
    }
}

io.captureStdout() and io.captureStderr() redirect the named stream into a memory buffer that you read with io.toString. Closing the capture restores the previous target. For more control, the lower-level io.redirectStdout(stream) / redirectStderr / redirectStdin family returns a restore callable; pair them with defer restore(); when a test temporarily swaps a stream.

See docs/user/stdlib/01-io.md (Streams And Capture) for the complete API and a worked memory-stream example.

Running the suite from make

If your project uses a Makefile, add:

test-lang: build
	./geblang test tests/

and depend make test on both test-lang and the Go test target. The Geblang reference repo's own Makefile is a working example.

Running tests in CI

geblang test exits with code 0 if every test passes, 1 if any test fails, and 2 on usage errors. The summary line prints tests: total=<N> failed=<M> passed=<N-M> on stdout, so CI scripts can both rely on the exit code and parse the summary if they need it.

Test layout convention

The reference project structure groups tests by feature area:

tests/
  core/            # syntax, control flow, operators
  types/           # narrowing, aliases, optional
  generics/        # generic functions, reified runtime checks
  classes/         # inheritance, interfaces, immutability
  functions/       # closures, overloading, decorators
  async_generators/
  errors/
  regex/
  stdlib/          # one file per stdlib module
  check/           # files that must fail `geblang check`

Files under check/ are not part of the regular geblang test run; they exercise geblang check's static diagnostics. The reference Makefile has a check-lang target that drives a script which asserts every file under tests/check/ produces a diagnostic while every other test file checks clean.

Reified generics in tests

The runtime enforces declared element types for list<T>, set<T>, and dict<K,V>. A test can confirm the enforcement:

@test
func listRejectsWrongElement(): void {
    let threw = false;
    try {
        list<int> bad = [1, "two", 3];
    } catch (RuntimeError e) {
        threw = true;
    }
    this.assertTrue(threw);
}

These tests act as guards against regressions in the type checker (the static side) and the reified-generics runtime (the dynamic side).