testpick: Run Only the Tests Your Diff Can Actually Break
testpick is a test-selection CLI for JavaScript and TypeScript. It looks at what you changed in git diff and runs just the tests affected by those changes — turning multi-minute CI runs into seconds.
$ npx testpick map # one-time: learn which tests touch which code $ npx testpick run # from now on: run only what your changes affect
What Is testpick?
testpick is an open-source, coverage-based test-selection tool for JavaScript and TypeScript projects. Instead of re-running your entire test suite on every change, it figures out which tests can actually be affected by your git diff and runs only those — so a CI job that used to take six minutes can finish in seconds.
The difference from static approaches is how testpick builds its map: from runtime coverage — what each test actually executed — rather than a static import graph. That lets it capture couplings a static analyzer can't see (like a module loaded through a runtime-computed path) while staying safe: when a change touches something it hasn't mapped, testpick runs more tests, never fewer.
How It Works
Three commands, zero configuration. testpick auto-detects your runner (Jest or Vitest) and any workspaces.
Map
Run testpick map once. It measures runtime coverage and records which tests exercise which source files.
Change
Edit your code as usual. testpick reads your git diff to see exactly which files moved.
Run
Run testpick run and only the affected test files execute. Use explain to see why.
$ testpick map Mapping 23 test file(s) with vitest in 8 single-pass shard(s). ✔ Map saved to .testpick/map.json — 24 source files tracked. $ vim src/util.ts # change one file... $ testpick run testpick: 1/23 test file(s) affected by 1 change(s). ✓ src/greet.test.ts (1 test) 4ms $ testpick explain # ...and see *why* Changed files (1): • src/util.ts Decisions: ✓ src/util.ts [coverage map → 1 test(s)] → src/greet.test.ts Result: run 1 of 23 test file(s).
Key Features
Runtime Coverage Map
Selection is built from what each test actually executed — so it catches runtime-computed couplings (plugin registries, DI containers, dynamic imports of a path that's data) that a static import graph can't see.
Safety First
The rule is simple: when in doubt, run more — never less. A change to an unmapped file (new file, config) runs the whole suite by default, so testpick never silently skips a test that matters.
Fast, Single-Pass Maps
Instead of starting the runner once per file, testpick shards files across your cores and runs each shard as one process — far fewer startups, measurably faster wall-clock and less total CPU.
Incremental & Parallel
Each test file is hashed, so only changed or new files are re-measured — a no-op refresh is instant. Mapping runs up to one lane per CPU, tunable with -j.
Monorepo Aware
Detects npm, yarn, and pnpm workspaces and treats each package as its own unit — its own runner, its own map. Vitest in one package and Jest in another both work, selected per package.
Explainable Selection
testpick explain is a dry run that prints exactly which tests were selected or skipped and the reason for each decision — so you can trust the selection before you rely on it in CI.
Why Not Just vitest --changed or jest --onlyChanged?
Those flags are great — until your code has couplings their static import graph can't see. A module loaded via a runtime-computed path (a plugin registry, a DI container, or import(pathFromConfig)) is invisible to static analysis: Vite can glob a literal dynamic import, but it can't analyze a path that is data.
// Vite's static graph can't see this — the path is data, not a literal const REGISTRY = { feat: "../features/feat.ts" }; export const load = (n) => import(/* @vite-ignore */ REGISTRY[n]);
testpick builds its map from what each test actually ran, so it captures that edge. Change features/feat.ts and static selection finds nothing to run — testpick still runs the test that loads it:
Change features/feat.ts |
Result |
|---|---|
| vitest related features/feat.ts | No test files found ✗ |
| testpick run | Runs loader.test.ts ✓ |
testpick and a static module graph are complementary, not strictly better: a coverage map is more precise for runtime-computed couplings and for trimming imported-but-unexecuted modules, while a static graph can flag a yet-to-run branch a coverage map hasn't seen. That's exactly why testpick errs toward running more — and why it works the same for Jest, where there is no Vite graph.
Commands & Options
| Command | What it does |
|---|---|
testpick map | Build or refresh the coverage map (incremental by default). |
testpick run | Run only the tests affected by your changes. |
testpick explain | Dry run — print the selection and the reasoning behind it. |
| Option | Meaning |
|---|---|
--base <ref> | Diff against a ref (in CI: --base origin/main). Default: working tree vs HEAD. |
--ai | Use an LLM (needs ANTHROPIC_API_KEY) to narrow unmapped changes — but it can never cause a skip. |
--all | Escape hatch: run the whole suite. |
--full | map only: rebuild from scratch instead of incrementally. |
-j, --jobs <n> | map only: max concurrent coverage passes (default: CPU count). |
In CI (GitHub Actions)
Point testpick at the pull request's base branch and it runs only the tests the PR can affect. Commit .testpick/map.json to share the map across runs, or rebuild it on a schedule.
- run: npm ci - run: npx testpick run --base origin/${{ github.base_ref }}
Who Uses testpick?
testpick pays off anywhere the test suite has outgrown the change being tested.
- Teams with slow CI — Cut multi-minute pull-request checks down to the handful of tests a diff can actually break, without giving up safety.
- Monorepo maintainers — Per-package selection means changing one package doesn't run another's tests, and mixed Jest/Vitest setups just work.
- Large or legacy suites — Get fast, targeted feedback on big codebases where running everything on each change is impractical.
- TDD & local loops — Keep a tight edit-run cycle locally by running only what your current change touches.
- Projects with dynamic wiring — Plugin registries, DI containers, and runtime-computed imports stay covered because selection is based on real execution.
Frequently Asked Questions
npx testpick or add it to your project's dev dependencies.vitest --changed or jest --onlyChanged?--ai flag can narrow unmapped changes, but if the model is unsure it still falls back to running everything, so the AI can never cause a skip. testpick explain shows exactly why each test was selected or skipped.npx testpick run --base origin/<base_ref> after installing dependencies. Commit the generated .testpick/map.json to share the coverage map across CI runs, or rebuild it on a schedule. testpick then runs only the tests affected by the pull request's diff.npx testpick map then npx testpick run.