Askr
Guides

Testing Deterministic Applications

Use testing deterministic applications with the current published Askr packages.

  • @askrjs/askr0.0.59

How to use testing deterministic applications

Control clocks, requests, and route input in tests; assert rendered behavior and cleanup instead of implementation call counts.

  1. Import the published @askrjs/askr entrypoint.
  2. Keep testing deterministic applications configuration next to the component, route, or server composition root that owns it.
  3. Handle pending, unavailable, cancellation, and failure states, then verify the production path.
import { state } from '@askrjs/askr';

it('should show the new quantity given increment when clicked', () => {
  const quantity = state(1);
  quantity.set(quantity() + 1);
  expect(quantity()).toBe(2);
});

Goal and architecture

`@askrjs/askr/testing` and `@askrjs/askr/fx` exist specifically so async and time-based code doesn't make your test suite flaky: `mockQuery`/`queryState` let you construct a query in any state (`fresh`, `loading`, `error`, `refreshing`, `stale`, `pendingWrite`) without a real network call, and the `fx` timing helpers (`debounce`, `throttle`, `retry`, `timeout`) are pure functions you can control directly rather than fighting real timers. The goal is that a test asserts on a specific, reproducible state rather than racing against real I/O or `setTimeout`.

Implementation

Use `mockQuery.error(error, previousData)` and `mockQuery.stale(data, reason)` to exercise UI branches that are otherwise hard to hit reliably, and `createInvalidationRecorder()` to assert exactly which cache prefixes a mutation invalidated instead of re-running real queries to check for side effects. For route logic, `matchRoute(path, { manifest })` and `getRouteWarnings()` let you assert on route resolution and catch route-collision warnings without booting a browser. For timing-dependent code built on `retry()` or `debounce()`, pass an injected `delayMs`/`backoff` or drive the returned `cancel()`/`flush()` handles directly rather than relying on wall-clock waits in tests.

Failure states

A test that mocks a query as `fresh` but never exercises the `loading` or `error` states is testing the easy 80% of the component; use the full `queryState` matrix deliberately so regressions in error and stale-data rendering get caught. Real timers (`setTimeout`, `requestAnimationFrame`) in tests are a common source of flakiness — prefer the `fx` primitives' explicit `cancel()`/`flush()` surface or a fake-timer setup over sprinkling `await timeout(50)` calls that pass locally and flake in CI.

Verification

Treat `createInvalidationRecorder()` assertions as part of your contract for mutations — a mutation that's supposed to invalidate a `'users'` prefix should have a test asserting exactly that prefix appears in `recorder.calls`, not just that the mutation resolved. Snapshot route warnings from `getRouteWarnings()` in CI so a newly introduced route collision fails the build instead of surfacing as a confusing 404 in production.