TEST SELECTION
How to run only the tests affected by a PR
Updated July 2026 · 7 min read
Running the whole suite on every pull request is the correct answer right up until it isn't — when the suite takes 40 minutes, CI bills bite, and devs start batching changes to avoid the wait. Then you need selection: run the tests this change could actually affect. Here are the three real approaches and their honest trade-offs.
1. Dependency-graph selection (build-system level)
Tools like Nx, Turborepo, and Bazel model your workspace as a graph of packages. Change package A, and everything that imports A is "affected" — run those packages' tests, skip the rest.
- Great for: monorepos with clean package boundaries; unit/integration tests colocated with packages.
- Weakness: granularity. Touch one file in a hub package and the graph selects half the repo. And it only sees import edges — an E2E test that navigates to
/checkouthas no import relationship with the checkout page component.
2. Coverage-based selection (execution level)
Instrument a full run once, record which tests execute which lines, then map changed lines → tests that covered them. This is what Jest's --changedSince approximates and what commercial "predictive test selection" products do with ML on top.
- Great for: unit tests in one language, stable codebases where the coverage map stays fresh.
- Weakness: the map goes stale with every merge; instrumentation slows runs; and for browser E2E, "coverage" is smeared across the whole app — everything covers everything, so selection degenerates to "run it all." Cross-repo: impossible, the map doesn't span repos.
3. Static impact analysis (artifact level)
Skip execution entirely. Read the diff, extract the concrete artifacts tests bind to — data-testid attributes, ARIA labels, element ids, route strings, visible text — and string-match them against test files. If a PR removes data-testid="checkout-submit", any spec referencing it is affected. No instrumentation, no graph, works in seconds.
- Great for: E2E/UI suites (which bind to exactly these artifacts), cross-repo setups, and getting a signal at review time rather than run time.
- Weakness: it's textual. Dynamic selectors (
`row-${id}`) and purely behavioral changes (a function's logic changes but no selector/route does) won't match. It tells you what's structurally endangered, not everything that could fail.
Which one should you use?
| Dependency graph | Coverage map | Static impact | |
|---|---|---|---|
| Unit tests | ✅ best | ✅ good | ➖ weak |
| E2E / UI tests | ➖ blind | ➖ smeared | ✅ best |
| Cross-repo suites | ❌ no | ❌ no | ✅ yes |
| Setup cost | build migration | instrumentation | ~zero |
| When signal arrives | CI time | CI time | review time |
They compose: graph selection for unit tests, static impact for the E2E layer. The mistake is using only the first two and concluding E2E selection is impossible — it isn't, it just needs a different mechanism.
The review-time advantage
Approaches 1–2 answer "which tests should CI run?" Static analysis answers something better: "which tests did this PR just endanger?" — while the PR is still open. That's the difference between a faster red build and no red build: the author sees "this breaks checkout.spec.ts in the e2e repo" and fixes the spec in the same change.
This is what Testward automates as a GitHub App: anchor extraction from the diff, matching against your spec files (including across repos), an LLM pass to filter false positives, and one PR comment naming the endangered specs. You can try the extraction on any diff in your browser — no install — with the free diff scanner.
Install Testward free — the endangered specs show up as a comment before CI even starts.
Install free on GitHub