Testing Utilities Design
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
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:
Mock Hierarchy
This diagram shows how the mock utilities relate to the real implementations they replace:
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.tsCross-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