Initial commit

This commit is contained in:
Zhongwei Li
2025-11-29 18:01:45 +08:00
commit 23371433fd
17 changed files with 6735 additions and 0 deletions

View File

@@ -0,0 +1,491 @@
# XState v5 Testing Reference (Backend Focus)
This reference covers testing XState v5 actors and state machines for **backend applications**. For comprehensive actor testing patterns, see the [xstate-audition skill](../../xstate-audition/SKILL.md).
## Testing Strategy Overview
XState v5 testing for backend applications focuses on:
- Actor lifecycle and behavior testing
- State transition verification
- Event handling and inter-actor communication
- Asynchronous operation testing
- Error handling and recovery
## Recommended Testing Library: xstate-audition
For comprehensive actor testing, use **[xstate-audition](https://boneskull.github.io/xstate-audition/)**, which provides:
- Dependency-free XState v5 actor testing
- Async-first testing utilities
- Comprehensive actor lifecycle testing
- Built-in timeout handling
- Curried functions for test reusability
See the [xstate-audition skill documentation](../../xstate-audition/SKILL.md) for detailed usage patterns.
## Basic Unit Testing
While xstate-audition excels at actor testing, you may need to unit test individual pieces:
### Testing Guards in Isolation
```typescript
import { describe, test, expect } from 'vitest';
// Guard function
const isValidThreshold = ({ context }: { context: { value: number } }) =>
context.value > 10;
describe('guards', () => {
test('isValidThreshold returns true for values > 10', () => {
expect(isValidThreshold({ context: { value: 15 } })).toBe(true);
expect(isValidThreshold({ context: { value: 5 } })).toBe(false);
});
});
```
### Testing Action Functions
```typescript
import { vi, describe, test, expect } from 'vitest';
// Action function
const logMessage = ({ context, event }: any) => {
console.log(`Event: ${event.type}, Count: ${context.count}`);
};
test('logMessage action logs correctly', () => {
const consoleSpy = vi.spyOn(console, 'log');
logMessage({
context: { count: 5 },
event: { type: 'TEST_EVENT' },
});
expect(consoleSpy).toHaveBeenCalledWith('Event: TEST_EVENT, Count: 5');
});
```
## Backend-Specific Testing Patterns
### Testing Database Transaction States
```typescript
import { createActor } from 'xstate';
import { runUntilDone, waitForSnapshot } from 'xstate-audition';
const dbTransactionMachine = createMachine({
initial: 'idle',
states: {
idle: {
on: { BEGIN: 'transaction' },
},
transaction: {
initial: 'processing',
states: {
processing: {
invoke: {
src: 'executeQuery',
onDone: 'committing',
onError: 'rollingBack',
},
},
committing: {
invoke: {
src: 'commitTransaction',
onDone: '#db.success',
onError: '.rollingBack',
},
},
rollingBack: {
invoke: {
src: 'rollbackTransaction',
onDone: '#db.failed',
},
},
},
},
success: { type: 'final' },
failed: { type: 'final' },
},
});
test('successful database transaction', async () => {
const actor = createActor(dbTransactionMachine);
const promise = waitForSnapshot(actor, (s) => s.matches('success'));
actor.send({ type: 'BEGIN' });
await promise;
expect(actor.getSnapshot().value).toBe('success');
});
```
### Testing Message Queue Processing
```typescript
const queueProcessorMachine = createMachine({
context: {
messages: [],
processed: 0,
failed: 0,
},
initial: 'polling',
states: {
polling: {
invoke: {
src: 'pollQueue',
onDone: {
target: 'processing',
actions: assign({
messages: ({ event }) => event.output,
}),
},
},
},
processing: {
always: [
{
target: 'idle',
guard: ({ context }) => context.messages.length === 0,
},
{
target: 'processingMessage',
},
],
},
processingMessage: {
invoke: {
src: 'processMessage',
input: ({ context }) => context.messages[0],
onDone: {
target: 'processing',
actions: assign({
messages: ({ context }) => context.messages.slice(1),
processed: ({ context }) => context.processed + 1,
}),
},
onError: {
target: 'processing',
actions: assign({
messages: ({ context }) => context.messages.slice(1),
failed: ({ context }) => context.failed + 1,
}),
},
},
},
idle: {
after: {
5000: 'polling',
},
},
},
});
test('processes queue messages', async () => {
const actor = createActor(queueProcessorMachine);
await waitForSnapshot(actor, (snapshot) => snapshot.context.processed >= 3);
expect(actor.getSnapshot().context.failed).toBe(0);
});
```
### Testing API Rate Limiting States
```typescript
const rateLimiterMachine = createMachine({
context: {
requestCount: 0,
resetTime: null,
},
initial: 'ready',
states: {
ready: {
on: {
REQUEST: [
{
target: 'throttled',
guard: ({ context }) => context.requestCount >= 100,
actions: assign({
resetTime: () => Date.now() + 60000,
}),
},
{
target: 'processing',
actions: assign({
requestCount: ({ context }) => context.requestCount + 1,
}),
},
],
},
},
processing: {
invoke: {
src: 'handleRequest',
onDone: 'ready',
onError: 'ready',
},
},
throttled: {
after: {
60000: {
target: 'ready',
actions: assign({
requestCount: 0,
resetTime: null,
}),
},
},
},
},
});
test('enforces rate limits', async () => {
const actor = createActor(rateLimiterMachine);
// Send 100 requests
for (let i = 0; i < 100; i++) {
actor.send({ type: 'REQUEST' });
}
// 101st request should be throttled
actor.send({ type: 'REQUEST' });
await waitForSnapshot(actor, (s) => s.matches('throttled'));
expect(actor.getSnapshot().context.resetTime).toBeDefined();
});
```
## Testing with Mock Services
### Mocking External Services
```typescript
import { vi } from 'vitest';
import { fromPromise } from 'xstate';
test('handles service failures gracefully', async () => {
const mockService = vi
.fn()
.mockRejectedValue(new Error('Service unavailable'));
const machine = setup({
actors: {
externalService: fromPromise(mockService),
},
}).createMachine({
initial: 'calling',
states: {
calling: {
invoke: {
src: 'externalService',
onError: {
target: 'fallback',
actions: assign({
error: ({ event }) => event.error.message,
}),
},
},
},
fallback: {
type: 'final',
},
},
});
const actor = createActor(machine);
const result = await runUntilDone(actor);
expect(actor.getSnapshot().context.error).toBe('Service unavailable');
expect(mockService).toHaveBeenCalledTimes(1);
});
```
## Testing Async Behavior
### Using waitFor from xstate
```typescript
import { waitFor } from 'xstate';
test('waits for async completion', async () => {
const actor = createActor(asyncMachine);
actor.start();
actor.send({ type: 'START_ASYNC' });
const snapshot = await waitFor(
actor,
(snapshot) => snapshot.context.isComplete === true,
{ timeout: 5000 },
);
expect(snapshot.context.result).toBeDefined();
});
```
### Testing with Fake Timers
```typescript
import { vi, beforeEach, afterEach } from 'vitest';
describe('delayed transitions', () => {
beforeEach(() => {
vi.useFakeTimers();
});
afterEach(() => {
vi.useRealTimers();
});
test('transitions after delay', () => {
const actor = createActor(delayedMachine);
actor.start();
expect(actor.getSnapshot().value).toBe('waiting');
vi.advanceTimersByTime(5000);
expect(actor.getSnapshot().value).toBe('ready');
});
});
```
## Testing Actor Communication
### Testing Parent-Child Actors
```typescript
import { spawn, sendTo } from 'xstate';
import { runUntilSpawn, waitForSnapshot } from 'xstate-audition';
test('parent spawns and communicates with child', async () => {
const parentMachine = createMachine({
initial: 'idle',
context: {
childResponse: null,
},
states: {
idle: {
on: {
START: {
actions: spawn(childMachine, { id: 'child' }),
target: 'waiting',
},
},
},
waiting: {
on: {
CHILD_RESPONSE: {
actions: assign({
childResponse: ({ event }) => event.data,
}),
target: 'done',
},
},
entry: sendTo('child', { type: 'PROCESS' }),
},
done: { type: 'final' },
},
});
const actor = createActor(parentMachine);
const childRef = await runUntilSpawn(actor, 'child');
await waitForSnapshot(actor, (s) => s.matches('done'));
expect(actor.getSnapshot().context.childResponse).toBeDefined();
});
```
## Performance Testing
```typescript
test('handles high throughput', async () => {
const start = performance.now();
const actor = createActor(highThroughputMachine);
actor.start();
// Send many events rapidly
for (let i = 0; i < 10000; i++) {
actor.send({ type: 'PROCESS', data: i });
}
await waitFor(actor, (s) => s.context.processedCount === 10000);
const duration = performance.now() - start;
expect(duration).toBeLessThan(5000); // Should process within 5 seconds
});
```
## Error Handling Tests
```typescript
test('recovers from errors with retry', async () => {
let attempts = 0;
const mockService = vi.fn().mockImplementation(() => {
attempts++;
if (attempts < 3) {
throw new Error('Temporary failure');
}
return { success: true };
});
const retryMachine = createMachine({
context: { retries: 0, maxRetries: 3 },
initial: 'attempting',
states: {
attempting: {
invoke: {
src: fromPromise(mockService),
onDone: 'success',
onError: [
{
target: 'attempting',
guard: ({ context }) => context.retries < context.maxRetries,
actions: assign({
retries: ({ context }) => context.retries + 1,
}),
},
{
target: 'failed',
},
],
},
},
success: { type: 'final' },
failed: { type: 'final' },
},
});
const actor = createActor(retryMachine);
await runUntilDone(actor);
expect(actor.getSnapshot().value).toBe('success');
expect(mockService).toHaveBeenCalledTimes(3);
});
```
## Test Organization Best Practices
1. **Separate unit tests from integration tests** - Test guards and actions separately from full actor behavior
2. **Use xstate-audition for actor tests** - Leverage its powerful async utilities
3. **Mock external dependencies** - Database, APIs, message queues
4. **Test error paths explicitly** - Ensure graceful degradation
5. **Use descriptive test names** - Clearly indicate what behavior is being tested
6. **Keep tests focused** - One behavior per test
7. **Use fake timers for time-dependent logic** - Control time progression explicitly
## Summary
For backend XState v5 testing:
- Use **[xstate-audition](../../xstate-audition/SKILL.md)** for comprehensive actor testing
- Focus on actor behavior, not UI interactions
- Test database transactions, message processing, and API interactions
- Mock external services appropriately
- Handle async behavior with proper utilities
- Ensure error recovery and retry logic works correctly