Change-Aware Test Selection
also called Affected Test Selection, Predictive Test Selection
Running only the tests a change could affect, determined from a dependency graph - with periodic full runs to verify the selection itself.
In a large repository, most changes affect a small part of the system. Running every test on every change means every developer pays for every component, and the pipeline's duration is set by the slowest area regardless of what was touched.
Test selection uses a dependency graph — which components a change can reach — to run only the affected tests. For a large monorepo this frequently converts the dominant pipeline cost into a fraction of itself.
Why the graph is the hard part
The value and the risk both come from the same place: if the graph is wrong, a change breaks something the pipeline did not test.
Dependencies are not only code imports. They include configuration, generated code, shared test fixtures, data files, build definitions, and runtime dependencies expressed nowhere in the source. A graph derived only from imports will under-approximate.
Implementation patterns
- Be conservative. When in doubt, include. An over-inclusive selection costs time; an under-inclusive one costs correctness.
- Periodic full runs, on a schedule and on merge to trunk, which catch what selection missed and detect drift in the graph as the codebase changes.
- Measure selection accuracy — how often does a full run fail where the selective run passed? That number is the honest measure of whether the graph is trustworthy.
- Include non-code dependencies explicitly: configuration files, fixtures, build definitions, container base images.
- Combine with a merge queue, which verifies the actual post-merge combination — selection makes each verification fast, the queue makes the result trustworthy.
- Fall back to full on ambiguity, such as a change to a build definition or a shared tool.
Industry example
Developer platforms operating large monorepos rely on this heavily, because the alternative is a pipeline whose duration grows with the repository rather than with the change. It is what makes fast feedback possible at scale.
The staging that accompanies it matters as much: static checks and unit tests for affected components in the first minutes, broader integration and contract verification after, and end-to-end tests for critical journeys on merge rather than on every push. Fast feedback first, comprehensive later — because a developer gets actionable information quickly and the slow parts do not block the loop.
The metric that matters is the proportion of pipeline time spent on components the change could not affect, which is the direct measure of whether selection is working.
Failure scenarios
- An incomplete graph, silently skipping tests that would have caught the defect.
- No full-run baseline, so selection accuracy is unmeasured and drift is invisible.
- Selection applied to the merge check, where the combination — not the individual change — is what needs verifying.
- Non-code dependencies omitted, so a configuration change is treated as affecting nothing.
- Selection used to hide a slow suite rather than to complement fixing it.
Trade-offs
Selection trades a small correctness risk for a large speed gain, and the risk is managed rather than eliminated. It also requires maintaining the graph, which is real ongoing work as the codebase evolves.
For a small repository with a fast suite it is unnecessary. It becomes valuable when the full suite is slow enough to change developer behaviour — which is the same threshold at which batching begins, and the reason it is worth doing before the suite gets slower.
Interview question
"Your test suite takes forty minutes and developers have started batching changes. Walk me through what you would do — and tell me how you would know your test selection is not silently skipping something important."