Testing

Snapshot testing, session recording, replay, and deterministic assertions.

2 min read 480 words

Milo ships with testing utilities purpose-built for the Elm Architecture. Test reducers with action sequences, test views with snapshots, test sagas step-by-step, and record/replay entire sessions.

Testing strategies

flowchart TB subgraph Unit RS[assert_state] --> Reducer RN[assert_renders] --> Template SG[assert_saga] --> Saga end subgraph Integration Rec[recording_middleware] --> Session[JSONL Session] Session --> Replay[milo replay] end Replay -->|"--assert"| CI[CI Regression]

Snapshot testing

Render state through a template and compare to a snapshot file:

from milo.testing import assert_renders

assert_renders(
    {"count": 5, "label": "Total"},
    "counter.kida",
    snapshot="tests/snapshots/counter_5.txt",
)

On first run, the snapshot file is created. On subsequent runs, the output is compared to the stored snapshot. ANSI codes are stripped by default.

MILO_UPDATE_SNAPSHOTS=1 pytest
pytest  # Fails if snapshots don't match

Reducer testing

Feed an action sequence through a reducer and assert the final state:

from milo.testing import assert_state
from milo import Action

assert_state(
    reducer,
    None,  # initial state
    [Action("@@INIT"), Action("INCREMENT"), Action("INCREMENT")],
    {"count": 2},  # expected final state
)

Note

assert_statereplays actions synchronously — no event loop, no rendering, no sagas. This isolates the reducer logic for fast, deterministic tests.

Saga testing

Step through a saga generator, asserting each yielded effect:

from milo.testing import assert_saga
from milo import Call, Put, Action

assert_saga(
    fetch_saga(),
    [
        (Call(fetch_json, ("https://api.example.com",), {}), {"data": 42}),
        (Put(Action("DATA_LOADED", payload={"data": 42})), None),
    ],
)

Each tuple is (expected_effect, value_to_send_back). The test runner asserts the yielded effect matches, then sends the value back into the generator.

Session recording

Record every action dispatched during an interactive session:

app = App(template="app.kida", reducer=reducer,
          initial_state=None, record=True)
app.run()  # Writes to session.jsonl
app = App(template="app.kida", reducer=reducer,
          initial_state=None, record="my_session.jsonl")
app.run()

The recording middleware captures each action with a state hash in JSONL format.

Session replay

Replay a recorded session for debugging or CI regression testing:

# Normal replay at 2x speed
milo replay session.jsonl --speed 2.0

# Show state diffs between actions
milo replay session.jsonl --diff

# Step-by-step interactive replay
milo replay session.jsonl --step

# CI mode: assert state hashes match
milo replay session.jsonl --assert --reducer myapp:reducer

Warning

The--assertflag compares state hashes at each step against the recorded values. If you change your reducer logic, recorded sessions will fail hash checks — re-record affected sessions after intentional changes.