TestStore
TestStore wraps Store.create() with assertions and an async send() that flushes microtasks. It is the main utility for integration testing — send an intent, verify events were emitted and state is correct.
import { TestStore } from '@hurum/core/testing'import { PurchaseStore } from './store'import { PurchaseIntents } from './intents'import { PurchaseEvent } from './events'
const mockRepo = { save: vi.fn().mockResolvedValue(savedPurchase),}
const store = TestStore(PurchaseStore, { initialState: { purchase: mockPurchase }, deps: { repository: mockRepo },})TestStore accepts the same options as Store.create():
| Option | Type | Description |
|---|---|---|
initialState | Partial<RawState> | Deep merged with store defaults |
deps | Partial<Deps> | Shallow merged with store defaults |
send (async)
Section titled “send (async)”send() dispatches an intent and waits for all async executors to complete before resolving. This is the key difference from the regular store.send() — you can await it and then assert.
// Named shorthandawait store.send.submitClicked({ purchase: mockPurchase })
// PreparedIntentawait store.send(PurchaseIntents.submitClicked({ purchase: mockPurchase }))
// Descriptor + payloadawait store.send(PurchaseIntents.submitClicked, { purchase: mockPurchase })All three forms are equivalent. The named shorthand is the most concise.
Assertions
Section titled “Assertions”assertState
Section titled “assertState”Check that specific state fields match expected values. Uses structural equality — you only need to provide the fields you want to check.
await store.send.submitClicked({ purchase: mockPurchase })
store.assertState({ saving: false, purchase: savedPurchase,})// Only checks `saving` and `purchase`. Other fields are ignored.If a field doesn’t match, the error message shows both expected and received values:
State mismatch for key "saving": Expected: false Received: trueassertEvents
Section titled “assertEvents”Verify the exact list of events that were emitted, in order.
await store.send.submitClicked({ purchase: mockPurchase })
store.assertEvents([ PurchaseEvent.saveRequested({ id: '123' }), PurchaseEvent.saved({ purchase: savedPurchase }),])This checks both the event types and their payloads. Order matters — the events must appear in exactly the order shown.
assertEventSequence
Section titled “assertEventSequence”Check events paired with the state snapshot after each event. This is the most powerful assertion — it verifies that intermediate states were correct throughout the flow.
await store.send.submitClicked({ purchase: mockPurchase })
store.assertEventSequence([ { event: PurchaseEvent.saveRequested({ id: '123' }), state: { saving: true, error: null }, }, { event: PurchaseEvent.saved({ purchase: savedPurchase }), state: { saving: false, purchase: savedPurchase }, },])Each entry is matched independently — you can check any subset of the event sequence. The state field is a partial match, just like assertState.
assertNoRunningExecutors
Section titled “assertNoRunningExecutors”Verify all executors have completed. Useful at the end of a test to ensure no async work is still pending.
await store.send.submitClicked({ purchase: mockPurchase })store.assertNoRunningExecutors()If executors are still running:
Expected no running executors, but 2 are still runningOther methods
Section titled “Other methods”getState
Section titled “getState”Read the full current state (raw + computed):
const state = store.getState()expect(state.purchase?.name).toBe('Test')expect(state.totalAmount).toBe(300) // computedAccess nested child store instances:
const childStore = store.scope.items// Interact with nested stores for complex scenariosFull example
Section titled “Full example”import { describe, it, expect, vi } from 'vitest'import { TestStore } from '@hurum/core/testing'import { PurchaseStore } from './store'import { PurchaseIntents } from './intents'import { PurchaseEvent } from './events'
describe('PurchaseStore', () => { const mockPurchase = { id: '123', name: 'Widget', items: [{ amount: 100 }] }
it('saves a purchase', async () => { const savedPurchase = { ...mockPurchase, savedAt: '2026-01-01' } const mockRepo = { save: vi.fn().mockResolvedValue(savedPurchase) }
const store = TestStore(PurchaseStore, { initialState: { purchase: mockPurchase }, deps: { repository: mockRepo }, })
await store.send.submitClicked({ purchase: mockPurchase })
store.assertEventSequence([ { event: PurchaseEvent.saveRequested({ id: '123' }), state: { saving: true }, }, { event: PurchaseEvent.saved({ purchase: savedPurchase }), state: { saving: false, purchase: savedPurchase }, }, ]) store.assertNoRunningExecutors() })
it('handles save failure', async () => { const mockRepo = { save: vi.fn().mockRejectedValue(new Error('Network error')) }
const store = TestStore(PurchaseStore, { deps: { repository: mockRepo }, })
await store.send.submitClicked({ purchase: mockPurchase })
store.assertState({ saving: false, error: 'Network error' }) })})Next steps
Section titled “Next steps”- TestExecutor — Unit test executors in isolation
- TestReducer — Test individual on() handlers