Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Testing

Keel test blocks make agent-facing code deterministic without replacing your production program.

use std/ai
use std/testing

type Severity = low | medium | critical

task classify(text: str) -> Severity {
  ai.classify(text, as: Severity) ?? Severity.low
}

test "mocked classify returns critical" {
  testing.mock(ai.classify).returns(Severity.critical)
  assert classify("payment outage") == Severity.critical
}

Run tests with:

keel test triage.keel

Run one group by name with:

keel test triage.keel --filter classify

List tests without running them with:

keel test triage.keel --list

Test Blocks

Test blocks live at the top level beside type, task, and agent declarations:

test "name" {
  assert true
}

keel test type-checks each tested file, registers declarations, and runs each test block. Top-level statements such as run(MyAgent) are skipped unless a test calls them explicitly. keel run ignores test blocks.

Pass a directory to recursively run .keel files with test blocks. Files without test blocks are skipped.

--filter <text> runs only tests whose names contain text. Matching is case-sensitive, and the command fails if no tests match.

--list prints matching test names without running them. It can be combined with --filter. If a file has no test blocks, keel test prints 0 tests found and exits successfully.

--fail-fast stops after the first failing test. --quiet suppresses passing test result lines while still printing failures and the final summary.

Each executed test line includes elapsed time after the test name. The final summary includes total suite time, and failures print the source location when available before returning a failing exit status.

Assertions

assert expr

The expression must be bool. If it evaluates to false, the test fails.

test "math" {
  assert 2 + 2 == 4
}

Use a custom failure message with a second str expression:

test "math" {
  assert 2 + 2 == 5, "expected arithmetic to balance"
}

Parameterized Tests

Use for name in cases after the test name to run one test case for each item in a list:

test "validate status" for case in [
  { score: 95, expected: Status.valid },
  { score: 150, expected: Status.needs_review }
] {
  assert validate_score(case.score) == case.expected
}

The runner prints each case with an index, such as validate status [0]. The case binding is available in setup and in the test body.

Setup

Use setup to prepare values that the test body can assert against:

use std/ai

test "summary" {
  setup {
    expected: str = "short"
    actual: str = ai.summarize("long article") ?? ""
  }

  assert actual == expected
}

Mocks

Mocks replace stdlib module methods inside one test:

use std/ai
use std/testing

test "summary fallback" {
  testing.mock(ai.summarize).returns("short")
  assert ai.summarize("long article") == "short"
}

For enum classification, return the enum variant directly:

use std/ai
use std/testing

test "classification" {
  testing.mock(ai.classify).returns(Severity.critical)
  assert classify("payment outage") == Severity.critical
}

Mocks are scoped to a single test. If two tests mock the same method differently, each test sees only its own value.

Repeat a mock target to return a sequence of values. Once the sequence is exhausted, the last value repeats:

use std/ai
use std/testing

test "summaries" {
  testing.mock(ai.summarize).returns("first")
  testing.mock(ai.summarize).returns("second")

  assert ai.summarize("a") == "first"
  assert ai.summarize("b") == "second"
  assert ai.summarize("c") == "second"
  assert ai.summarize.called
  assert ai.summarize.call_count == 3
  assert ai.summarize.called_with("a")
}

Mocked methods expose test-local metadata:

use std/ai
use std/testing

test "draft" {
  testing.mock(ai.draft).returns("Thanks")

  reply = ai.draft("response to Ada", tone: "friendly") ?? ""

  assert reply == "Thanks"
  assert ai.draft.called
  assert ai.draft.call_count == 1
  assert ai.draft.called_with("response to Ada", tone: "friendly")
}

called_with(...) returns true when any recorded mock call matches the supplied evaluated arguments. Positional arguments match from the start, and named arguments match by name.

@tools capability checks still apply. A mock changes the method result; it does not grant an agent access to a namespace that its @tools block disallows.

Contextual Syntax

test, setup, and assert are not reserved keywords. They are recognized only in their testing positions, so existing identifiers with those names remain valid elsewhere.