Testing Utilities Design
February 13, 2026 · View on GitHub
Low-level design for @stateloom/testing — the test utility package for consumers testing against StateLoom reactive primitives and stores.
Design Goals
- Core-only dependency — peer-depends on
@stateloom/coreonly. No framework or paradigm imports. - Structural typing — mock stores match
StoreApi<T>without importing@stateloom/store. - Test ergonomics —
reset()on every mock for cleanbeforeEachteardown. - Zero magic — all mocks are manually controlled. No auto-tracking, no reactive graph integration.
Architecture
graph TD
subgraph "@stateloom/testing"
TS[createTestScope]
MS[mockSubscribable]
MSt[mockStore]
CV[collectValues]
FE[flushEffects]
WU[waitForUpdate]
end
subgraph "@stateloom/core"
CS[createScope]
RIS[runInScope]
Sub["Subscribable<T>"]
end
TS --> CS
TS --> RIS
CV --> Sub
WU --> Sub
style TS fill:#4a9eff,color:#fff
style MS fill:#7c3aed,color:#fff
style MSt fill:#7c3aed,color:#fff
style CV fill:#059669,color:#fff
style FE fill:#059669,color:#fff
style WU fill:#059669,color:#fff
Dependency Boundary
The package imports from @stateloom/core for:
createScope/runInScope— used bycreateTestScope()Subscribable<T>/Signal<T>/Scope— type-only imports for interfaces
It does not import from @stateloom/store, @stateloom/atom, or any framework adapter. The MockStoreApi<T> interface declares its own method signatures that structurally match StoreApi<T>.
Type Strategy
Structural Compatibility
MockStoreApi<T> declares the same surface as StoreApi<T>:
interface MockStoreApi<T extends Record<string, unknown>> {
get(): T;
getState(): T;
setState(partial: Partial<T> | ((state: T) => Partial<T>)): void;
subscribe(listener: (state: T, prevState: T) => void): () => void;
getInitialState(): T;
destroy(): void;
// Test helpers (not on StoreApi)
getStateHistory(): readonly T[];
reset(): void;
}
TypeScript's structural typing makes MockStoreApi<T> assignable to StoreApi<T> at call sites without any explicit extends relationship. The extra methods (getStateHistory, reset) don't break assignability.
MockSubscribable vs Subscribable
MockSubscribable<T> declares get() and subscribe() matching Subscribable<T>, plus emit(), getSubscriberCount(), and reset(). It is structurally assignable to Subscribable<T>.
Implementation Details
createTestScope — Scope Wrapping
ScopeImpl uses private #fields, so external code cannot clear its internal Map. Instead of trying to reset the existing scope, reset() replaces the entire scope reference:
function wrapScope(coreScope: Scope): TestScope {
let scope = coreScope;
return {
get scope() {
return scope;
},
reset() {
scope = createScope();
},
// ... delegates to scope
};
}
mockStore — Shallow Merge Semantics
setState follows the same semantics as @stateloom/store:
const resolved = typeof partial === 'function' ? partial(state) : partial;
state = Object.assign({}, state, resolved);
This creates a new reference on every update, matching real store behavior.
collectValues — Extended Array
Returns a real Array extended via Object.assign with an unsubscribe property. This preserves all array methods (.length, .filter, .map, indexing) while adding cleanup capability.
waitForUpdate — Timeout + Auto-Cleanup
Uses setTimeout for timeout and auto-unsubscribes in all code paths (resolution and rejection). A settled flag prevents double-resolution race conditions.
Deferred: TestScopeProvider
A React <TestScopeProvider> component was considered but deferred because it would require a React peer dependency, violating the core-only constraint. Options for the future:
@stateloom/react-testing— dedicated React testing package- Part of
@stateloom/react— bundled with the React adapter
Test Utility Decision Tree
Use this flowchart to choose the right testing utility:
flowchart TB
Start["What are you testing?"] --> Q1{Component with stores?}
Start --> Q2{Reactive state logic?}
Start --> Q3{Async state transitions?}
Q1 -->|Yes| MS["mockStore(initialState)<br/>Structural match for StoreApi"]
Q2 -->|Yes| Q4{Need isolation?}
Q3 -->|Yes| WU["waitForUpdate(subscribable, timeout)<br/>Promise-based with auto-cleanup"]
Q4 -->|Per-test scope| TS["createTestScope()<br/>reset() per test"]
Q4 -->|Track emitted values| CV["collectValues(subscribable)<br/>Array + unsubscribe"]
Q4 -->|Mock a subscribable| MSub["mockSubscribable(initial)<br/>emit() + getSubscriberCount()"]
Q1 --> Q5{Need to flush effects?}
Q2 --> Q5
Q5 -->|Yes| FE["flushEffects()<br/>Process pending microtasks"]
Mock Hierarchy
This diagram shows how the mock utilities relate to the real implementations they replace:
classDiagram
class Subscribable~T~ {
<<interface>>
+get() T
+subscribe(cb) () => void
}
class StoreApi~T~ {
<<interface>>
+get() T
+getState() T
+setState(partial) void
+subscribe(listener) () => void
+destroy() void
}
class MockSubscribable~T~ {
+get() T
+subscribe(cb) () => void
+emit(value) void
+getSubscriberCount() number
+reset() void
}
class MockStoreApi~T~ {
+get() T
+getState() T
+setState(partial) void
+subscribe(listener) () => void
+destroy() void
+getStateHistory() T[]
+reset() void
}
class TestScope {
+scope Scope
+fork() Scope
+get(sub) T
+set(signal, value) void
+reset() void
}
Subscribable~T~ <|.. MockSubscribable~T~ : structurally matches
StoreApi~T~ <|.. MockStoreApi~T~ : structurally matches
MockSubscribable~T~ : "Test control: emit(), reset()"
MockStoreApi~T~ : "Test control: getStateHistory(), reset()"
TestScope : "Test control: reset() replaces scope"
Coverage Strategy
| Test Category | Target | Utilities Used | Example |
|---|---|---|---|
| Unit: signal/computed/effect | 95%+ branches | collectValues, flushEffects | Verify computed recomputes on signal change |
| Unit: store setState | 95%+ branches | mockStore or real store | Verify shallow merge semantics |
| Unit: middleware hooks | 95%+ branches | Real store + middleware | Verify onSet chain ordering |
| Integration: framework adapter | 90%+ | mockSubscribable, framework test utils | Verify re-render on state change |
| Integration: SSR | Key paths | createTestScope, framework SSR utils | Verify scope isolation per request |
| Type: structural compatibility | All exported types | expectTypeOf assertions | Verify MockStoreApi assignable to StoreApi |
File Structure
packages/testing/
├── src/
│ ├── types.ts # All exported interfaces
│ ├── test-scope.ts # createTestScope()
│ ├── mock-subscribable.ts # mockSubscribable()
│ ├── mock-store.ts # mockStore()
│ ├── collect-values.ts # collectValues()
│ ├── flush-effects.ts # flushEffects()
│ ├── wait-for-update.ts # waitForUpdate()
│ ├── global.d.ts # Runtime globals (queueMicrotask, setTimeout)
│ └── index.ts # Barrel export
├── __tests__/
│ ├── test-scope.test.ts
│ ├── mock-subscribable.test.ts
│ ├── mock-store.test.ts
│ ├── collect-values.test.ts
│ ├── flush-effects.test.ts
│ ├── wait-for-update.test.ts
│ └── types.test.ts
├── package.json
├── tsconfig.json
├── tsup.config.ts
└── vitest.config.ts
Cross-References
- Architecture Overview — where testing fits in the layer structure
- Core Design —
Subscribable<T>,Scopeinterfaces that testing mocks - Store Design —
StoreApi<T>interface thatmockStorematches structurally - Layer Scoping — testing depends only on core (no paradigm or framework imports)
- Testing Guidelines — consumer-facing testing standards
- API Reference:
@stateloom/testing— consumer-facing documentation