Testing best practices (Vitest + Testing Library + Wallaby)
This project uses Vitest for unit/integration tests with a jsdom environment, plus optional Wallaby for fast, in-editor feedback. This guide captures the rationale behind our config and the patterns we use to keep tests reliable, readable, and fast.
Determinism and order
- Shuffle test execution to uncover hidden state coupling
- Enabled via
test.sequence.shuffle = true
- Enabled via
- Use a stable seed in CI for reproducible runs
test.sequence.seed = 20251111whenTF_BUILDis set
- Prefer isolated tests; avoid reliance on execution order
Deterministic date handling (UTC)
- Treat timezone-less ISO strings as UTC and append
Zbefore parsing- Canonicalize with
new Date(iso).toISOString()to normalize output
- Canonicalize with
- Use UTC getters (
getUTCHours,getUTCFullYear, etc.) consistently - Prefer lexicographic sorting for date-only keys (
YYYY-MM-DD) to avoid environment-dependent parsing - Enforce
process.env.TZ = 'UTC'intests/vitest/vitest-setup.ts
Note: We normalize timezone-less ISO strings by appending Z and then
canonicalizing with toISOString(). This guarantees UTC interpretation and
prevents environment-dependent parsing. Grouping utilities and render logic
apply this normalization before any date math or comparisons.
References: Vitest sequence docs (shuffle/seed), reporters and CI usage.
Isolation and hygiene
- No globals:
test.globals = false; import what you use - Global setup (
tests/vitest/vitest-setup.ts):vi.resetAllMocks(),vi.clearAllMocks()inbeforeEachvi.useRealTimers()to avoid timer leakage- Register matchers with Vitest-friendly import:
import '@testing-library/jest-dom/vitest'
- Raise listener limit to avoid EventEmitter warnings in parallel runs:
process.setMaxListeners(64)- Rationale: multiple suites attach process-level listeners (e.g., SIGINT, SIGTERM) under parallel execution. Raising the cap removes noisy MaxListeners warnings without masking legitimate errors.
- Prefer module-level pure helpers over process-level shared state
References: jest-dom “With Vitest” usage notes.
Async UI assertions (if/when testing React)
- Use
screenqueries from Testing Library - Prefer
findBy*(auto-retrying) for async UI instead of manual timeouts - Use
waitFor(...)to wrap stateful async updates when needed - Prefer
userEventoverfireEventfor realistic interactions - Query by role/name first; fall back to
getByTestIdonly when necessary
References: React Testing Library cheatsheet (queries, async, events).
Coverage policy
- Provider:
v8 - Thresholds (global + per-file):
- branches: 75
- lines/functions/statements: 80
- Reports:
- Local:
html+text-summary→./coverage - CI:
html,lcov,text-summary→./test-results/coverage
- Local:
- Exclusions: type defs, test files, build outputs, config files
References: Vitest coverage config and reporters.
CI behavior
- Reporters:
defaultlocally;['junit', 'default']in CI- JUnit output:
./test-results/junit.xml
- JUnit output:
- Concurrency: threads pool, capped at 8 workers
- Determinism: stable seed and
allowOnly = false - Retries: enabled only in CI (
retry = 2) for transient flake
Standard CI env is exported via the composite action
./.github/actions/standard-ci-env, which sets TZ=UTC and TF_BUILD=true
before running installs/build/tests. This keeps CI output and snapshot behavior
consistent across jobs.
References: Vitest reporters, sequence, retries.
jsdom environment notes
- No real layout/rendering; APIs like
getBoundingClientRect()may be zeroed - Use role/name-based queries; avoid layout expectations
- If you need
requestAnimationFrame, jsdom can simulate it; but we avoid enablingpretendToBeVisualto keep runs lean - Avoid global window mutation; prefer dependency injection
References: jsdom README (caveats, pretendToBeVisual, executing scripts).
Wallaby tips (optional local TDD)
Wallaby auto-detects Vitest config. If you add a manual config, prefer:
autoDetect: ['vitest'](or leave auto detection on)- Keep workers small while starting (
workers: 1–2) if you see flake - If transient issues occur, try
workers.restart = true - Exclude large folders from instrumentation (dist, coverage, caches)
- Use the Wallaby Troubleshooting guide for stuck cache or core updates
References: Wallaby overview, auto-detect, workers, troubleshooting.
Quick checklist
- Use
screenqueries; preferfindBy*for async - Reset/clear mocks and real timers in
beforeEach - Avoid global state and implicit test ordering
- Don’t assert on DOM structure; assert on behavior and accessible roles
- Keep coverage thresholds green; raise on critical paths when feasible
- In CI, keep runs reproducible (seed, junit, capped threads)
Further reading:
- React Testing Library: queries, async, and patterns
- @testing-library/jest-dom: matchers and Vitest setup
- jsdom: environment options, limitations, and safety
- Wallaby: configuration, workers, and troubleshooting