Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/Augani/kael/llms.txt

Use this file to discover all available pages before exploring further.

Kael ships first-class test infrastructure alongside the framework itself. The test-support feature flag unlocks a TestAppContext that replaces the real foreground and background executors with a deterministic in-process scheduler. This means your tests exercise real entity mutations, real subscriptions, and real async tasks — without flakiness from OS scheduling or real timers. Because the output of the #[kael::test] macro is a standard Rust test function, every existing test runner — cargo test, cargo-nextest, and others — works without any configuration changes.

Enabling test support

Add the test-support feature to your [dev-dependencies]:
toml
[dev-dependencies]
kael = { version = "*", features = ["test-support"] }
Do not add test-support to your [dependencies] or the main [features] table. It is intended exclusively for test binaries.

The #[kael::test] macro

Annotate any async test function with #[kael::test]. The macro injects a TestAppContext (or multiple, for collaborative UI tests) and wires up the deterministic executor:
rust
use kael::TestAppContext;

#[kael::test]
async fn test_counter_increments(cx: &TestAppContext) {
    // cx is a fully functional app context backed by the test executor.
    assert!(true);
}
For tests that simulate two independent clients or windows, request multiple contexts:
rust
use kael::TestAppContext;

#[kael::test]
async fn test_collaboration(cx_a: &TestAppContext, cx_b: &TestAppContext) {
    // cx_a and cx_b share the same deterministic executor but have
    // independent entity registries, matching the real multi-window model.
    assert!(true);
}

Writing unit tests for entities

Use cx.update to interact with the app context from inside a test. Create entities, mutate them, and assert on their state:
1

Define your model

rust
struct Counter {
    value: i32,
}
2

Create an entity in the test context

rust
#[kael::test]
async fn test_entity_mutation(cx: &TestAppContext) {
    let counter = cx.update(|cx| cx.new(|_cx| Counter { value: 0 }));
3

Mutate and read the entity

rust
    cx.update(|cx| {
        counter.update(cx, |model, _cx| {
            model.value += 1;
        });
    });

    let value = cx.update(|cx| counter.read(cx).value);
    assert_eq!(value, 1);
}

Observing entities with observe

The kael::test::observe helper converts entity change notifications into a futures::Stream so you can await them inside an async test:
rust
use kael::{TestAppContext, Entity};
use kael::test::observe;
use futures::StreamExt as _;

struct Flag {
    raised: bool,
}

#[kael::test]
async fn test_observation(cx: &mut TestAppContext) {
    let flag = cx.update(|cx| cx.new(|_| Flag { raised: false }));

    // Build a stream that emits () on every change notification.
    let mut changes = observe(&flag, cx);

    // Mutate the entity from another context (simulating a background task).
    cx.update(|cx| {
        flag.update(cx, |model, cx| {
            model.raised = true;
            cx.notify();
        });
    });

    // The stream produces one item per notification.
    changes.next().await;

    let raised = cx.update(|cx| flag.read(cx).raised);
    assert!(raised);
}
observe returns an Observation<T> which holds a Subscription internally. When the Observation is dropped, the subscription is cancelled automatically.

Simulating events in tests

The deterministic executor lets you advance time or flush pending tasks explicitly. Combine this with cx.update to simulate sequences of UI events:
rust
use kael::TestAppContext;

struct InputBuffer {
    text: String,
}

impl InputBuffer {
    fn push_char(&mut self, c: char, _cx: &mut kael::ModelContext<Self>) {
        self.text.push(c);
    }
}

#[kael::test]
async fn test_input_sequence(cx: &TestAppContext) {
    let buffer = cx.update(|cx| cx.new(|_| InputBuffer { text: String::new() }));

    cx.update(|cx| {
        buffer.update(cx, |model, cx| model.push_char('h', cx));
        buffer.update(cx, |model, cx| model.push_char('i', cx));
    });

    let text = cx.update(|cx| buffer.read(cx).text.clone());
    assert_eq!(text, "hi");
}

Controlling randomness and retries

The test runner inside kael::test supports seed-based iteration for fuzz-style tests. You can override the seed and iteration count with environment variables:
bash
# Run the test with a specific seed.
SEED=42 cargo test -p kael my_test_name

# Run 100 iterations with randomly chosen seeds.
ITERATIONS=100 cargo test -p kael my_test_name
When a seeded run fails, the runner prints the failing seed to stderr:
failing seed: 42
Re-run with SEED=42 to reproduce the failure deterministically.

Running tests

cargo test -p kael
The worker_process and extension_process integration tests spin up real child processes and verify the full IPC transport path. They are slower than unit tests and require the host binary to be built first. Run them before cutting a release.

Reference

SymbolLocationPurpose
TestAppContextkael::TestAppContextDrop-in replacement for AppContext backed by a deterministic executor
TestDispatcherkael::TestDispatcherSeed-driven scheduler used internally by run_test
observekael::test::observeConverts entity notifications into a futures::Stream
Observation<T>kael::test::ObservationStream wrapper that cancels its subscription on drop
run_testkael::test::run_testLow-level harness; prefer the #[kael::test] macro
#[kael::test]kael-macrosProc-macro that injects TestAppContext and wires the executor

Build docs developers (and LLMs) love