# Pattern Testing
Write automated tests for patterns using the same reactive system.
## Prerequisites
Before writing tests, ensure your pattern:
1. **Uses `pattern()`** - Single-type patterns can't be properly tested
2. **Exports actions as `Stream`** - Required for `instance.action.send()` to work
```typescript
// Pattern must have explicit Output type with Stream for testable actions
interface MyOutput {
count: number;
increment: Stream; // ← Enables testing via .send()
}
export default pattern(({ count }) => {
const increment = incrementHandler({ count });
return { count, increment }; // ← Must return bound handler
});
```
If your pattern uses `pattern()` or doesn't export actions, fix the pattern first. See [Pattern Types](../concepts/pattern.md#always-use-dual-type-parameters).
## Overview
Test patterns are patterns that test other patterns. They:
- Import and instantiate the pattern under test
- Define test actions using `action()`
- Define assertions using `computed(() => boolean)`
- Return a `tests` array that the runner executes sequentially
Test files end in `.test.tsx` and are run with `deno task ct test`.
## Quick Example
```tsx
///
import { action, computed, pattern } from "commontools";
import Counter from "./counter.tsx";
export default pattern(() => {
// 1. Instantiate pattern under test with plain values
const counter = Counter({ value: 0 });
// 2. Define actions (trigger events on the pattern)
const action_increment = action(() => {
counter.increment.send();
});
// 3. Define assertions (computed booleans)
const assert_is_zero = computed(() => counter.value === 0);
const assert_is_one = computed(() => counter.value === 1);
// 4. Return tests array
return {
tests: [
{ assertion: assert_is_zero },
{ action: action_increment },
{ assertion: assert_is_one },
],
};
});
```
**Note:** Pass plain values when instantiating patterns in tests. The runtime creates independent writable cells automatically. Use `Writable.of()` only when you need to test shared state behavior. See [Writable](../concepts/types-and-schemas/writable.md#passing-values-to-pattern-inputs) for details.
## Running Tests
```bash
# Run a specific test
deno task ct test packages/patterns/my-pattern/main.test.tsx
# Run with verbose output
deno task ct test packages/patterns/my-pattern/main.test.tsx --verbose
# Run all tests in a directory
deno task ct test packages/patterns/my-pattern/
```
## Test Step Format
Tests use a **discriminated union** format:
```tsx
return {
tests: [
{ action: action_do_something }, // Runner calls .send()
{ assertion: assert_something }, // Runner checks === true
],
};
```
Each step is either `{ action: Stream }` or `{ assertion: boolean }`.
## Writing Actions
Use `action()` to create void streams that trigger events on the pattern:
```tsx
// Trigger a void handler
const action_reset = action(() => {
game.reset.send(); // No argument needed for Stream
});
// Trigger a handler with data
const action_add_item = action(() => {
list.addItem.send({ name: "Test Item", quantity: 5 });
});
// Multiple operations in one action
const action_setup_game = action(() => {
game.playerReady.send();
game.startGame.send();
});
```
## Writing Assertions
Use `computed()` to create reactive boolean assertions:
```tsx
// Simple equality
const assert_count_is_5 = computed(() => counter.value === 5);
// Complex conditions
const assert_all_items_valid = computed(() => {
return list.items.every(item => item.quantity > 0);
});
// Multiple conditions
const assert_game_ready = computed(() => {
return game.phase === "ready" && game.players.length === 2;
});
```
## Test Organization
### Naming Conventions
Use descriptive names that explain what the test verifies:
```tsx
// Actions: action_
const action_add_first_item = action(() => { ... });
const action_remove_all_items = action(() => { ... });
// Assertions: assert_
const assert_list_empty = computed(() => list.items.length === 0);
const assert_total_is_100 = computed(() => cart.total === 100);
```
### Logical Ordering
Put actions before the assertions that depend on them:
```tsx
return {
tests: [
// Initial state
{ assertion: assert_starts_empty },
// Add items
{ action: action_add_item },
{ assertion: assert_has_one_item },
// Modify items
{ action: action_update_item },
{ assertion: assert_item_updated },
// Remove items
{ action: action_remove_item },
{ assertion: assert_empty_again },
],
};
```
## Debugging Failed Tests
When a test fails, use the CLI to inspect pattern state:
### 1. Run with Verbose Mode
```bash
deno task ct test ./main.test.tsx --verbose
```
This shows which action ran before each assertion failure.
### 2. Deploy and Inspect
Deploy the test pattern and use CLI commands to inspect state:
```bash
# Deploy the test pattern
deno task ct piece new ./main.test.tsx
# Get the piece ID from the output, then inspect
deno task ct piece inspect --piece
# Get specific values
deno task ct piece get subject/items --piece
# Step through manually
deno task ct piece call tests/0/action --piece
deno task ct piece step --piece
deno task ct piece get tests/1/assertion --piece
```
### 3. Expose Debug Data
Add extra fields to your test pattern for debugging:
```tsx
return {
tests: [...],
// Expose internals for debugging
subject,
debugState: computed(() => ({
phase: game.phase,
turn: game.currentTurn,
scores: game.scores,
})),
};
```
## Common Patterns
### Testing Initial State
```tsx
// Verify pattern initializes correctly
const assert_initial_count = computed(() => counter.value === 0);
const assert_initial_empty = computed(() => list.items.length === 0);
return {
tests: [
{ assertion: assert_initial_count },
{ assertion: assert_initial_empty },
// ... actions and more assertions
],
};
```
### Testing State Transitions
```tsx
const action_start = action(() => game.start.send());
const action_pause = action(() => game.pause.send());
const action_resume = action(() => game.resume.send());
const assert_playing = computed(() => game.phase === "playing");
const assert_paused = computed(() => game.phase === "paused");
return {
tests: [
{ action: action_start },
{ assertion: assert_playing },
{ action: action_pause },
{ assertion: assert_paused },
{ action: action_resume },
{ assertion: assert_playing },
],
};
```
### Testing Computed Values
```tsx
const action_add_items = action(() => {
cart.addItem.send({ price: 10, quantity: 2 });
cart.addItem.send({ price: 5, quantity: 4 });
});
const assert_total_correct = computed(() => {
// 10*2 + 5*4 = 40
return cart.total === 40;
});
```
## Best Practices
1. **Self-contained test data**: Keep test data inside actions, not external variables
2. **One thing per assertion**: Each assertion should verify one specific condition
3. **Meaningful names**: Names should describe expected state, not implementation
4. **Test edge cases**: Include actions for empty states, boundaries, error conditions
5. **Expose subject**: Return the pattern under test for CLI debugging
## See Also
- [Testing Handlers via CLI](./handlers-cli-testing.md) - Manual CLI testing workflow
- [Pattern Testing Spec](../../specs/PATTERN_TESTING_SPEC.md) - Technical specification