@ng-spark/signal-store-testing
v1.0.2
Published
Type-safe testing utilities for NgRx Signal Store
Maintainers
Readme
@ng-spark/signal-store-testing
A type-safe testing utility library for NgRx Signal Store. Test your Signal Stores with ease using intuitive APIs for state management, computed signals, and method calls.
Features
✅ State Management - Read and manipulate Signal Store state with ease ✅ Computed Signals - Test computed signal values ✅ Method Calls - Invoke store methods in tests ✅ Async Waiting - Wait for state or computed signal conditions ✅ State History - Record and navigate state changes (time travel debugging) ✅ Type-Safe - Full TypeScript support with automatic type inference ✅ Jest & Vitest Support - Works with both Jest and Vitest testing frameworks
Installation
npm install @ng-spark/signal-store-testing --save-devRequirements
- Angular 19+ or 20+
- NgRx Signals 19+ or 20+
- Testing Framework: Jest 29+ OR Vitest 1.0+
- TypeScript 5.9+
Quick Start
import { TestBed } from '@angular/core/testing';
import { createSignalStoreTester } from '@ng-spark/signal-store-testing';
import { MyStore } from './my.store';
describe('MyStore', () => {
let tester: ReturnType<typeof createSignalStoreTester<InstanceType<typeof MyStore>>>;
beforeEach(() => {
TestBed.configureTestingModule({
providers: [MyStore],
});
const store = TestBed.inject(MyStore);
tester = createSignalStoreTester(store);
});
it('should manage state', () => {
// Read state
expect(tester.state.count).toBe(0);
// Update state
tester.patchState({ count: 5 });
expect(tester.state.count).toBe(5);
// Assert state
tester.expectState({ count: 5 });
});
it('should test computed signals', () => {
tester.patchState({ count: 10 });
tester.expectComputed('doubleCount', 20);
});
it('should call methods', () => {
tester.callMethod('increment');
expect(tester.state.count).toBe(1);
});
it('should wait for async conditions', async () => {
setTimeout(() => tester.patchState({ count: 42 }), 100);
await tester.waitForState({ count: 42 });
expect(tester.state.count).toBe(42);
});
});API Reference
Creating a Tester
createSignalStoreTester(store, options?)
Creates a tester instance for a Signal Store.
Parameters:
store: TStore- The Signal Store instance to testoptions?: StoreTesterOptions- Optional configurationautoDetectChanges?: boolean- Auto-detect state changes (default:true)recordHistory?: boolean- Enable state history recording (default:false)errorMessages?: object- Custom error messages
Returns: SignalStoreTester<TStore>
Example:
const store = TestBed.inject(CounterStore);
const tester = createSignalStoreTester(store, {
recordHistory: true
});State Management
tester.state
Read-only property that returns the current state of all signals.
Returns: Object containing all signal values (both state and computed)
Example:
expect(tester.state.count).toBe(0);
expect(tester.state.incrementBy).toBe(1);tester.setState(state)
Replace the entire state with new values.
Parameters:
state: Partial<StoreState<TStore>>- New state values
Example:
tester.setState({ count: 10, incrementBy: 2 });tester.patchState(partial)
Merge partial state update with current state.
Parameters:
partial: Partial<StoreState<TStore>>- Partial state to merge
Example:
tester.patchState({ count: 5 }); // Only updates counttester.expectState(expected)
Assert that the state matches the expected values using Jest's toMatchObject.
Parameters:
expected: Partial<StoreState<TStore>>- Expected state values
Example:
tester.expectState({ count: 42, label: 'Test' });tester.expectStateToContain(expected)
Assert that the state contains the expected fields using Jest's objectContaining.
Parameters:
expected: Partial<StoreState<TStore>>- Expected state values
Example:
// Only checks count, ignores other fields
tester.expectStateToContain({ count: 42 });Computed Signals
tester.getComputed(name)
Get the current value of a computed signal.
Parameters:
name: ComputedSignalNames<TStore>- Name of the computed signal
Returns: The unwrapped signal value
Example:
const doubleCount = tester.getComputed('doubleCount');
expect(doubleCount).toBe(20);tester.expectComputed(name, expected)
Assert that a computed signal has the expected value.
Parameters:
name: ComputedSignalNames<TStore>- Name of the computed signalexpected: T- Expected value
Example:
tester.expectComputed('doubleCount', 20);
tester.expectComputed('isPositive', true);Method Calls
tester.callMethod(name, ...args)
Call a store method with optional arguments.
Parameters:
name: MethodNames<TStore>- Name of the method...args- Method arguments (type-safe)
Returns: The method's return value
Example:
tester.callMethod('increment');
tester.callMethod('setCount', 42);
const result = tester.callMethod('addValue', 10);Async Operations
tester.waitForState(condition, options?)
Wait for the state to match a condition.
Parameters:
condition: Partial<StoreState<TStore>> | WaitCondition<StoreState<TStore>>- State to wait for or condition functionoptions?: WaitOptions- Wait optionstimeout?: number- Timeout in ms (default:5000)interval?: number- Polling interval in ms (default:50)timeoutMessage?: string- Custom timeout error message
Returns: Promise<void>
Examples:
// Wait for specific state
await tester.waitForState({ count: 42 });
// Wait using condition function
await tester.waitForState(state => state.count > 100);
// With custom timeout
await tester.waitForState({ loading: false }, { timeout: 10000 });tester.waitForComputed(name, condition, options?)
Wait for a computed signal to match a condition.
Parameters:
name: ComputedSignalNames<TStore>- Name of the computed signalcondition: T | WaitCondition<T>- Value to wait for or condition functionoptions?: WaitOptions- Wait options
Returns: Promise<void>
Examples:
// Wait for specific value
await tester.waitForComputed('doubleCount', 20);
// Wait using condition function
await tester.waitForComputed('doubleCount', value => value > 25);State History (Time Travel)
tester.startRecording()
Start recording state history for time travel debugging.
Returns: StateHistory<StoreState<TStore>>
Example:
const history = tester.startRecording();tester.stopRecording()
Stop recording state history.
Example:
tester.stopRecording();tester.getHistory()
Get the current state history (if recording).
Returns: StateHistory<StoreState<TStore>> | null
Example:
const history = tester.getHistory();
if (history) {
console.log(history.states.length);
}StateHistory API
When recording is enabled, you get a StateHistory object with these methods:
history.states
Read-only array of all recorded state entries.
Type: ReadonlyArray<StateHistoryEntry<TState>>
history.currentIndex
Current position in the history.
Type: number
history.currentState
The state at the current history index.
Type: TState
history.goBack()
Navigate to the previous state in history.
Example:
history.goBack();
expect(history.currentState.count).toBe(5);history.goForward()
Navigate to the next state in history.
Example:
history.goForward();
expect(history.currentState.count).toBe(10);history.goToIndex(index)
Jump to a specific state by index.
Parameters:
index: number- Target index
Example:
history.goToIndex(0); // Go to initial statehistory.reset()
Reset to the initial state (index 0).
Example:
history.reset();
expect(history.currentIndex).toBe(0);history.clear()
Clear all history except the initial state.
Example:
history.clear();
expect(history.states.length).toBe(1);Complete Example
import { TestBed } from '@angular/core/testing';
import { createSignalStoreTester } from '@ng-spark/signal-store-testing';
import { signalStore, withState, withComputed, withMethods, patchState } from '@ngrx/signals';
import { computed } from '@angular/core';
// Define your store
const CounterStore = signalStore(
{ providedIn: 'root' },
withState({ count: 0, incrementBy: 1 }),
withComputed(({ count }) => ({
doubleCount: computed(() => count() * 2),
isEven: computed(() => count() % 2 === 0),
})),
withMethods((store) => ({
increment(): void {
patchState(store, (state) => ({ count: state.count + state.incrementBy }));
},
setCount(value: number): void {
patchState(store, { count: value });
},
}))
);
// Test it
describe('CounterStore', () => {
let tester: ReturnType<typeof createSignalStoreTester<InstanceType<typeof CounterStore>>>;
beforeEach(() => {
TestBed.configureTestingModule({
providers: [CounterStore],
});
const store = TestBed.inject(CounterStore);
tester = createSignalStoreTester(store);
});
it('should test complete workflow', async () => {
// Initial state
expect(tester.state.count).toBe(0);
tester.expectComputed('doubleCount', 0);
tester.expectComputed('isEven', true);
// Update state
tester.patchState({ count: 5 });
tester.expectState({ count: 5 });
tester.expectComputed('isEven', false);
// Call methods
tester.callMethod('increment');
expect(tester.state.count).toBe(6);
// Wait for async
setTimeout(() => tester.callMethod('setCount', 100), 100);
await tester.waitForState({ count: 100 });
// Time travel
const history = tester.startRecording();
tester.patchState({ count: 10 });
tester.patchState({ count: 20 });
history.goBack();
expect(history.currentState.count).toBe(10);
});
});TypeScript Support
The library is fully typed and provides excellent type inference:
// Automatic type inference for store
const tester = createSignalStoreTester(store);
// State is typed
tester.state.count; // ✅ Type: number
tester.state.unknown; // ❌ TypeScript error
// Method calls are type-safe
tester.callMethod('setCount', 42); // ✅ Correct
tester.callMethod('setCount', 'invalid'); // ❌ TypeScript error
tester.callMethod('unknownMethod'); // ❌ TypeScript error
// Computed signals are type-safe
tester.getComputed('doubleCount'); // ✅ Returns: number
tester.expectComputed('isEven', true); // ✅ Type-safeTesting Framework Support
This library works with both Jest and Vitest. The API is identical for both frameworks.
Jest Setup
// jest.config.ts
export default {
preset: 'jest-preset-angular',
setupFilesAfterEnv: ['<rootDir>/setup-jest.ts'],
};Vitest Setup
// vitest.config.ts
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
globals: true,
environment: 'jsdom',
setupFiles: ['./setup-vitest.ts'],
},
});Both frameworks provide the expect API that this library uses for assertions.
Best Practices
Type Your Tester Variable
let tester: ReturnType<typeof createSignalStoreTester<InstanceType<typeof MyStore>>>;Use Specific Assertions
- Use
expectStatefor partial state matching - Use
expectStateToContainfor loose matching - Use direct property access for single values
- Use
Enable History Recording for Complex Tests
const tester = createSignalStoreTester(store, { recordHistory: true });Use Async Waiting for Side Effects
await tester.waitForState(state => state.loading === false);
License
MIT
Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
Support
If you encounter any issues or have questions, please file an issue on the GitHub repository.
Made with ❤️ by @ng-spark
