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.
- Import the published @askrjs/askr entrypoint.
- Keep testing deterministic applications configuration next to the component, route, or server composition root that owns it.
- 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.