> Part of the walkerOS documentation. Project overview and full index: <https://www.walkeros.io/llms.txt>

# walkerOS Testing Strategy

## Overview[​](#overview "Direct link to 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[​](#the-rules "Direct link to The Rules")

### Rule 1: Use `env` for Mocking, Not Jest[​](#rule-1-use-env-for-mocking-not-jest "Direct link to 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[​](#rule-2-link-tests-to-dev-examples "Direct link to 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';



// Use examples.env for mock environment

const testEnv = mockEnv(examples.env.push, interceptor);



// Assert against examples.events (documented expected output)

expect(calls[0].args).toEqual(examples.events.ga4PageView());



// Test with examples.mapping configurations

const config = { mapping: examples.mapping.ecommerce };
```

#### Step Examples with `it.each`[​](#step-examples-with-iteach "Direct link to step-examples-with-iteach")

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](https://www.walkeros.io/skills/walkeros-using-step-examples.md) for the full lifecycle including the Three Type Zones and naming conventions.

### Rule 3: Test Real Behavior, Not Mock Behavior[​](#rule-3-test-real-behavior-not-mock-behavior "Direct link to 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[​](#rule-4-test-first-watch-it-fail "Direct link to 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:**

1. Write failing test
2. Verify it fails for expected reason (missing feature, not typo)
3. Write minimal code to pass
4. Verify it passes
5. 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[​](#rule-5-no-test-only-methods-in-production-code "Direct link to Rule 5: No Test-Only Methods in Production Code")

Production classes shouldn't have methods only tests use.

**Wrong:**

```
class Session {

  destroy() {

    /* only used in tests */

  }

}
```

**Right:**

```
// In test-utils/

export function cleanupSession(session: Session) { ... }
```

### Rule 6: Verify Before Claiming Complete[​](#rule-6-verify-before-claiming-complete "Direct link to Rule 6: Verify Before Claiming Complete")

"Should pass now" is not verification.

**Process:**

1. Run the actual test command
2. Read the output
3. Confirm pass/fail count
4. Only then claim status

## When to Use Each Test Type[​](#when-to-use-each-test-type "Direct link to 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 "Direct link to 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:**

```
# Simulate a destination (mocks its push, captures API calls)

walkeros push flow.json -e '{"entity":"page","action":"view"}' --simulate destination.ga4



# Simulate a source (captures events it pushes, disables all destinations)

walkeros push flow.json --simulate source.browser



# Mock a destination with a specific return value

walkeros push flow.json -e event.json --mock destination.ga4='{"status":"ok"}'
```

**Programmatic usage:**

```
import { push } from '@walkeros/cli';



// Simulate a destination

const result = await push(

  'flow.json',

  { entity: 'page', action: 'view' },

  {

    simulate: ['destination.ga4'],

  },

);

// result.usage = API call tracking data from wrapEnv



// Simulate a source

const result = await push('flow.json', undefined, {

  simulate: ['source.browser'],

});

// result.captured = events captured from source env.push
```

**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-specific-approaches "Direct link to 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[​](#the-env-pattern-deep-dive "Direct link to The env Pattern Deep Dive")

### How env Works[​](#how-env-works "Direct link to How env Works")

Each destination/source defines an `env` type that specifies external dependencies:

```
// Destination-specific env type

export interface Env extends DestinationWeb.Env {

  window: {

    gtag: Gtag.Gtag;

    dataLayer: unknown[];

  };

  document: {

    createElement: (tagName: string) => HTMLElement;

    head: { appendChild: (node: unknown) => void };

  };

}
```

### mockEnv() Function[​](#mockenv-function "Direct link to 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 });

  // Optionally return a value

});



// Now use testEnv in your destination context

await destination.push(event, { ...context, env: testEnv });



// Assert on captured calls

expect(calls).toContainEqual({

  path: ['window', 'gtag'],

  args: ['event', 'purchase', expect.objectContaining({ value: 99.99 })],

});
```

### dev.ts Structure[​](#devts-structure "Direct link to dev.ts Structure")

Each package with external dependencies should have:

```
// src/dev.ts

export * as schemas from './schemas';

export * as examples from './examples';



// src/examples/index.ts

export * as env from './env';

export * as events from './events';

export * as mapping from './mapping';

export * as step from './step'; // Step examples { in, out }
```

### Testing Sources with Injected env[​](#testing-sources-with-injected-env "Direct link to 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.

Type the mock against the source's own `Env` type, not against the global `Window`. A source narrows `Env.window` to only the members it touches, so the mock satisfies that narrowed shape directly with **no `as unknown as Window` cast**:

```
import type { Env } from '../types'; // the source's narrowed Env



// Instead of mocking window.performance globally:

const env: Env = {

  window: {

    performance: {

      getEntriesByType: jest.fn().mockReturnValue([{ type: 'navigate' }]),

    },

    location: { href: 'https://test.com/' },

  },

};



await createSessionSource(collector, undefined, env);



// Instead of mocking express import, type the binding against the source's

// injected dependency type so the mock is assignable without a cast:

import type { SourceEnv } from './types';



const express: SourceEnv['express'] = Object.assign(

  jest.fn().mockReturnValue(mockApp),

  { json: jest.fn().mockReturnValue(middleware) },

);

await sourceExpress(createSourceContext({}, { express }));
```

This pattern avoids global state pollution between tests, enables simulation in non-browser environments, and stays cast-free because the source narrows its own `Env` to exactly the members it uses (the same declare-global + narrowed-`Env` approach destinations use — see [create-destination §3.3.1](https://www.walkeros.io/skills/walkeros-create-destination.md)).

## Red Flags - Stop and Fix[​](#red-flags---stop-and-fix "Direct link to 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[​](#commands "Direct link to Commands")

Verification tier (per `/workspaces/developer/AGENT.md` rule 11):

```
# L1, the default during a task: typecheck, lint, test for the touched package

cd /workspaces/developer/walkerOS

npm run verify:touched -- core

npm run verify:touched -- web-destination-gtag



# L2, at plan completion: only packages affected since origin/main

git fetch origin main --depth=1

npm run verify:affected



# L3, before pushing or marking PR-ready: critical path + affected

npm run test:smoke



# Single test file (still useful while iterating)

cd packages/<name> && npm run test -- path/to/file.test.ts



# Watch mode (single package)

cd packages/<name> && npm run test -- --watch
```

Avoid bare `npm run test` at root inside per-task steps. That is L4 (full suite, 10-15 min) and is reserved for plan completion when the plan touched shared infra, or for explicit user request.

## Reading a Step Back from the Collector[​](#reading-a-step-back-from-the-collector "Direct link to Reading a Step Back from the Collector")

When a test calls a step's raw push directly through the collector bag (`collector.sources.X.push`, `collector.destinations.X.push`, etc.), use the typed accessors from `@walkeros/core` instead of casting:

```
import { Source } from '@walkeros/core';



const src = Source.getSource<TestSourceTypes>(collector, 'testSource');

await src.push({ method: 'GET', path: '/api/data' });
```

Available helpers: `Source.getSource`, `Destination.getDestination`, `Transformer.getTransformer`, `Store.getStore`. Each accepts an optional type parameter to recover the per-step generic that the bag's index signature erases on read. Each throws `<Kind> not found: <id>` when the id is unknown.

Do not write `collector.sources.X.push as any` or `collector.sources.X.push as (rawData: ...) => Promise<...>`. The accessor exists exactly to remove that boundary cast.

## Destination Test Duplication, Pending Cleanup[​](#destination-test-duplication-pending-cleanup "Direct link to Destination Test Duplication, Pending Cleanup")

As of 2026-04-29, web and server destination tests share substantial scaffolding (init validation, missing-settings rejection, mock setup, push assertions) with no shared harness. Audit found roughly 1500-2500 lines of repetition across 47 destinations. A typed shared harness is planned as a follow-on initiative.

When writing a new destination test today:

* Keep destination-specific behavior (mapping rules, vendor-specific batch shapes, edge cases) in the test file.
* The boilerplate sections (env clone, `jest.clearAllMocks`, missing-required-settings, init returns valid contract) are candidates for extraction. Mirror the shape used by an existing destination of the same family (web vs server) so the future migration is mechanical.
* Do NOT invent a new private harness inside the destination. If the missing harness blocks you, flag it.

## Related Skills[​](#related-skills "Direct link to Related Skills")

* [walkeros-understanding-development](https://www.walkeros.io/skills/walkeros-understanding-development.md) - Development conventions and workflow

**Reference:**

* [AGENT.md](https://github.com/elbwalker/walkerOS/blob/main/AGENT.md) - Development guide
