Testing Deterministic Applications
Testing Deterministic Applications: a worked guide from route registry through to a production build.
Example
Control clocks, requests, and route input in tests; assert rendered behavior and cleanup instead of implementation call counts.
it('should increment the quantity', () => {
expect(incrementQuantity(1)).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 builder methods (.loading(), .error(), .refreshing(), .stale(), .pendingWrite(), or calling mockQuery(data) for the fresh case) construct a query in any state without a real network call — these are builder names, not a literal list of consistency values; the actual consistency field is the narrower 'fresh' | 'stale' | 'refreshing' | 'pending-write', with loading as its own separate boolean and an errored query landing on consistency: 'stale', staleReason: 'error' rather than a consistency: 'error' value that doesn't exist. For timing, @askrjs/askr/fx exports event wrappers such as debounceEvent and throttleEvent, scheduling helpers, retry, and timeout; keep a local helper for non-event debounce behavior.
Implementation
Use mockQuery.error(error, previousData) and mockQuery.stale(data, reason) — reason here is limited to 'aborted' | 'inconsistent', since 'error' has its own dedicated .error() constructor — 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, import matchRoute from @askrjs/askr/router and getRouteWarnings from @askrjs/askr/testing; matchRoute(path, { registry }) and getRouteWarnings({ registry }) let you assert on route resolution and catch route-collision warnings without booting a browser. retry() returns a Promise<T> you await and accepts delay and backoff options; throttle() and raf() return cancelable functions. debounceEvent(ms, handler) and throttleEvent(ms, handler) return cancelable event handlers, and debounceEvent() also exposes .flush().
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 mockQuery state 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({ registry }) in CI so a newly introduced route collision fails the build instead of surfacing as a confusing 404 in production.