| name | walkeros-testing-strategy |
| description | walkerOS uses a layered testing approach with built-in patterns for mocking and Use when this capability is needed. |
| metadata | {"author":"elbwalker"} |
walkerOS Testing Strategy
Overview
walkerOS uses a layered testing approach with built-in patterns for mocking and
documentation sync. This skill ensures tests are reliable, efficient, and
maintainable.
Core principle: Test real behavior using the env pattern, link to dev
examples, verify before claiming complete.
The Rules
Rule 1: Use env for Mocking, Not Jest
walkerOS has a built-in dependency injection pattern via env in context. This
is lighter than Jest mocks, enables documentation generation, and keeps tests in
sync with examples.
Wrong:
jest.mock('../ga4', () => ({ initGA4: jest.fn() }));
expect(initGA4).toHaveBeenCalledWith(...);
Right:
import { examples } from '../dev';
import { mockEnv } from '@walkeros/core';
const calls: Array<{ path: string[]; args: unknown[] }> = [];
const testEnv = mockEnv(examples.env.push, (path, args) => {
calls.push({ path, args });
});
await destination.push(event, { ...context, env: testEnv });
expect(calls).toContainEqual({
path: ['window', 'gtag'],
args: ['event', 'page_view', { page_title: 'Home' }],
});
Rule 2: Link Tests to dev Examples
The dev.ts export provides examples.env, examples.events,
examples.mapping, and examples.step. Using these in tests ensures
documentation stays in sync.
import { examples } from '../dev';
const testEnv = mockEnv(examples.env.push, interceptor);
expect(calls[0].args).toEqual(examples.events.ga4PageView());
const config = { mapping: examples.mapping.ecommerce };
Step Examples with it.each
Step examples (examples.step) provide { in, out } pairs for each step. Use
it.each to iterate over them:
import { examples } from '../dev';
describe('step examples', () => {
it.each(Object.entries(examples.step))(
'%s',
async (name, { in: input, out: expected }) => {
const result = await step.push(input, context);
if (expected === false) {
expect(result).toBe(false);
} else {
expect(result).toEqual(expected);
}
},
);
});
See using-step-examples for the full
lifecycle including the Three Type Zones and naming conventions.
Rule 3: Test Real Behavior, Not Mock Behavior
If you're asserting that a mock was called, you're testing the mock works, not
the code.
Red flags:
expect(mockFn).toHaveBeenCalled() without verifying the mock produces real
effects
- Assertions on
*-mock test IDs
- Tests that pass when mock is present, fail when removed
Fix: Test what the code actually does. If external APIs must be mocked,
verify the real API would receive correct data.
Rule 4: Test First, Watch It Fail
If you didn't see the test fail, you don't know it tests the right thing.
Process:
- Write failing test
- Verify it fails for expected reason (missing feature, not typo)
- Write minimal code to pass
- Verify it passes
- Refactor if needed
Red flags:
- Test passes immediately when written
- Can't explain why test failed
- "I'll add tests later"
Rule 5: No Test-Only Methods in Production Code
Production classes shouldn't have methods only tests use.
Wrong:
class Session {
destroy() {
}
}
Right:
export function cleanupSession(session: Session) { ... }
Rule 6: Verify Before Claiming Complete
"Should pass now" is not verification.
Process:
- Run the actual test command
- Read the output
- Confirm pass/fail count
- Only then claim status
When to Use Each Test Type
| Type | When to Add | Example |
|---|
| Integration | New usage pattern, new external API interaction, new data flow path | Collector → Destination → gtag() |
| Unit | Combinatorics, edge cases, pure function logic | Mapping variations, core utilities |
| Contract | Boundary validation | Destination output matches vendor API, source input validation |
Guideline: Integration tests prove things work when stuck together. Unit
tests efficiently cover variations. Contract tests catch API drift.
Simulation Testing
Simulation testing uses the CLI push command with --simulate flags. The
collector does not export a simulate() function — simulation is a CLI concern
that maps to mock/disabled config properties at runtime.
CLI usage:
walkeros push flow.json -e '{"entity":"page","action":"view"}' --simulate destination.ga4
walkeros push flow.json --simulate source.browser
walkeros push flow.json -e event.json --mock destination.ga4='{"status":"ok"}'
Programmatic usage:
import { push } from '@walkeros/cli';
const result = await push('flow.json', { entity: 'page', action: 'view' }, {
simulate: ['destination.ga4'],
});
const result = await push('flow.json', undefined, {
simulate: ['source.browser'],
});
Key points:
--simulate destination.X sets config.mock = {} on the target and
config.disabled = true on all other destinations
--simulate source.X wraps env.push with a capture function and disables
all destinations
- Destination
/dev env.push is auto-loaded to provide mock globals (fake
window.gtag, etc.)
- Returns
PushResult with result, captured (source), and usage
(destination)
- The
mockEnv() and env pattern examples above remain correct for unit testing
individual step functions directly
Package-Specific Approaches
| Package | Approach |
|---|
| core | Unit tests only - pure functions, no env needed |
| collector | Integration tests critical - input/output consistency is paramount |
| browser source | Maintain walker algorithm coverage |
| web destinations | Integration tests per unique pattern + unit tests for mappings, use env pattern |
| server destinations | Same as web destinations |
| cli/docker | Integration tests for spawn behavior, explore dev pattern to reduce duplication |
| sources | Contract tests for input validation, integration tests for event capture |
The env Pattern Deep Dive
How env Works
Each destination/source defines an env type that specifies external
dependencies:
export interface Env extends DestinationWeb.Env {
window: {
gtag: Gtag.Gtag;
dataLayer: unknown[];
};
document: {
createElement: (tagName: string) => HTMLElement;
head: { appendChild: (node: unknown) => void };
};
}
mockEnv() Function
The mockEnv() function from @walkeros/core creates a Proxy that intercepts
all function calls:
import { mockEnv } from '@walkeros/core';
const calls: Array<{ path: string[]; args: unknown[] }> = [];
const testEnv = mockEnv(examples.env.push, (path, args) => {
calls.push({ path, args });
});
await destination.push(event, { ...context, env: testEnv });
expect(calls).toContainEqual({
path: ['window', 'gtag'],
args: ['event', 'purchase', expect.objectContaining({ value: 99.99 })],
});
dev.ts Structure
Each package with external dependencies should have:
export * as schemas from './schemas';
export * as examples from './examples';
export * as env from './env';
export * as events from './events';
export * as mapping from './mapping';
export * as step from './step';
Testing Sources with Injected env
Sources accept platform dependencies via env. Mock window, document, or
library imports by passing them through env instead of mocking globals:
const mockWindow = {
performance: {
getEntriesByType: jest.fn().mockReturnValue([{ type: 'navigate' }]),
},
location: { href: 'https://test.com/' },
} as unknown as Window & typeof globalThis;
await createSessionSource(collector, undefined, { window: mockWindow });
const mockExpress = Object.assign(jest.fn().mockReturnValue(mockApp), {
json: jest.fn().mockReturnValue(middleware),
});
await sourceExpress(createSourceContext({}, { express: mockExpress as never }));
This pattern avoids global state pollution between tests and enables simulation
in non-browser environments.
Red Flags - Stop and Fix
- Using
jest.mock() for internal modules when env pattern is available
- Tests that don't import from
../dev
- Assertions only checking mock call counts
- Tests with extensive mock setup (>50% of test is setup)
- Test-only methods added to production classes
- Claiming tests pass without running them
Commands
npm run test
cd packages/[name] && npm run test
npm run test -- path/to/file.test.ts
npm run test -- --watch
Related Skills
Reference:
Converted and distributed by TomeVault — claim your Tome and manage your conversions.