Skip to content

Testing Utilities Design

Low-level design for @stateloom/testing — the test utility package for consumers testing against StateLoom reactive primitives and stores.

Design Goals

  1. Core-only dependency — peer-depends on @stateloom/core only. No framework or paradigm imports.
  2. Structural typing — mock stores match StoreApi<T> without importing @stateloom/store.
  3. Test ergonomicsreset() on every mock for clean beforeEach teardown.
  4. 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 by createTestScope()
  • 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>:

typescript
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:

typescript
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:

typescript
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:

  1. @stateloom/react-testing — dedicated React testing package
  2. 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 CategoryTargetUtilities UsedExample
Unit: signal/computed/effect95%+ branchescollectValues, flushEffectsVerify computed recomputes on signal change
Unit: store setState95%+ branchesmockStore or real storeVerify shallow merge semantics
Unit: middleware hooks95%+ branchesReal store + middlewareVerify onSet chain ordering
Integration: framework adapter90%+mockSubscribable, framework test utilsVerify re-render on state change
Integration: SSRKey pathscreateTestScope, framework SSR utilsVerify scope isolation per request
Type: structural compatibilityAll exported typesexpectTypeOf assertionsVerify 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