TESTING.md
May 11, 2026 · View on GitHub
Rule of thumb: A change isn't done until you have proven, with tests, that (a) it works, and (b) nothing else broke. This applies to every kind of change — code, docs, config, CI, release.
This document defines what "thorough testing" means for x402trace, change-type by change-type. It is the operational definition of the hard rule in CLAUDE.md.
Test categories
Unit tests — tests/unit/**
- Single function or single class
- No I/O, no network, no filesystem
- Mock all external dependencies (RPC, facilitator, env)
- Should run in < 100ms each
- Tool: vitest
- Run:
pnpm test:unit
Integration tests — tests/integration/**
- Multiple components working together
- May use real filesystem; may use mocked RPC and mocked facilitator (a local Express server in the test harness)
- Should run in < 5s each
- Run:
pnpm test:integration
End-to-end tests — tests/e2e/**
- Full pipeline against Base Sepolia
- Requires testnet wallet with USDC and an
BASE_RPC_URLset in CI secrets - Run in CI on push to
v1,staging,main— not on every PR (would burn faucet money and slow PR feedback) - Run locally:
pnpm test:e2e
Smoke tests — tests/smoke/**
--helpand--versionwork- CLI starts without crashing
- Default config loads
- Run on every PR
- Should run in < 10s total
- Run:
pnpm test:smoke
Manual checklist — every PR
The PR template enforces this. Each PR must check off:
- Ran
pnpm testlocally, all green - Ran
pnpm typecheck, no errors - Ran
pnpm lint, no errors - Manually verified the user-facing change (paste output in the PR)
- Updated relevant docs (README, SPEC, ARCHITECTURE, CLAUDE.md)
- Added/updated tests covering the change
What to test, by change type
Code change to an existing module
- Unit tests for the new behavior
- Unit tests for at least one error path the change introduces or could trigger
- Existing tests still pass (regression check)
- If the module has a public API: an integration test exercising it
- If the change is user-facing: a screenshot or terminal paste in the PR
New module
- Unit tests covering: happy path, error path, edge cases (empty input, malformed input, very-large input)
- Integration test against at least one real consumer
- Type definitions exported and used in at least one test (proves the types are usable from outside)
- Documentation in ARCHITECTURE.md updated
Bug fix
- A test that fails on the current main and passes on this branch
- Test name describes the bug clearly (
decoder rejects truncated signature without crashing, nottest bug fix 3) - Note in CHANGELOG.md under
[Unreleased] → Fixed
Documentation change
- Markdown linter passes (
pnpm lint:docs) - All internal links resolve
- External links resolve when run quarterly (
pnpm lint:links); not enforced per PR - Code blocks marked as runnable actually run
Configuration change (.env.example, tsconfig.json, package.json)
- Build still works:
pnpm build - Tests still pass:
pnpm test - If a new env var is introduced: documented in
.env.exampleand referenced in CLAUDE.md - If a new dependency is added: justified in the PR description (why this lib, why not the standard library)
CI change (.github/workflows/**)
- Open a draft PR first so the workflow runs
- Verify all matrix combinations pass
- Verify failure modes fail correctly — push a deliberately-broken commit to a throwaway branch and confirm CI fails red
Release change (version bump, CHANGELOG)
npm pack --dry-runshows the expected files (notests/, no.env, no source maps unless intended)pnpm testgreen across the full suitepnpm test:e2egreen on testnet- Install the packed tarball locally and run
--help:npm pack && npm install -g ./x402trace-*.tgz && x402trace --help
Coverage
- No enforced minimum until v0.2. Don't game coverage before there's signal about what matters.
- Coverage report runs in CI on every PR for visibility (via
vitest --coverage). - New code should not decrease overall coverage by more than 1 percentage point without explicit justification in the PR.
When tests are hard to write
If you find yourself unable to test a change cleanly, that's a design signal. Stop. Refactor for testability before continuing.
Common causes and fixes:
| Symptom | Cause | Fix |
|---|---|---|
| Need to mock 5 things to test 1 function | Tight coupling | Inject dependencies; pass them as parameters |
| Test depends on system time | Hardcoded Date.now() | Inject a Clock interface, pass real clock in prod, fake clock in tests |
| Test depends on the network | Direct fetch call | Inject an HTTP client |
| Test needs a specific Node version | Reliance on undocumented runtime behavior | Use stable APIs only |
| Test passes/fails based on order | Shared module state | Encapsulate state in a class or factory |
Untested code is code that we don't yet know works.
Test data
- Real x402 payloads captured from dogfooding go in
tests/fixtures/ - Redact private keys and signatures before committing fixtures. Run
pnpm lint:fixturesto catch obvious leaks. - One fixture per scenario, named for what it tests:
- ✅
payment-required-v2-base-sepolia.json - ❌
test1.json
- ✅
- When generating new fixtures, use the dogfood rig (X402-3) and
x402trace inspectto capture them deterministically.
Mocking guidelines
- Mock at the boundary, not deep inside. Mock the interface a module talks to the outside world through, not its internal helpers.
- For chain RPC: mock the viem client interface, not individual RPC method calls.
- For the facilitator: use a local Express server in integration tests, not deep mocks. The server can return canned responses but is a real HTTP target — this catches serialization bugs.
- For environment variables: use
vi.stubEnv(), never write toprocess.envdirectly in tests.
Running tests
pnpm test # all tests (no coverage)
pnpm test:unit # fast feedback while coding
pnpm test:integration # before pushing
pnpm test:e2e # before opening a release PR
pnpm test:smoke # sanity check
pnpm test:watch # TDD mode
pnpm test:coverage # with coverage report
CI
The CI workflow (.github/workflows/ci.yml, see Jira X402-18) runs on every PR:
- Lint (
pnpm lint) - Typecheck (
pnpm typecheck) - Unit tests
- Integration tests
- Smoke tests
- Build
The CI workflow runs additionally on push to v1, staging, main:
- E2E tests against Base Sepolia
- Coverage report
E2E and coverage do not run on PR commits to keep PR feedback fast (~2 minutes target).
Test naming
Tests are documentation. Their names should make a failing test report read like a list of broken promises:
✅ Good:
decoder rejects truncated signature without crashing
reconciliation reports 'settled' when facilitator timed out but tx confirmed
CLI exits with code 2 when --upstream is missing
❌ Bad:
test1
test decoder
should work
Anti-patterns
The following are not allowed:
test.skip(...)left in the codebase without a linked Jira ticket explaining whysetTimeoutin tests instead of awaiting promises- Tests that depend on test execution order
- Tests that hit mainnet
- Tests that hit a real third-party facilitator on every run (use the local mock server)
- "I'll write the tests in the next PR"