When we make a change to code, we should only run tests that can be proven to be relevant. Running unrelated tests tells you nothing useful, wastes electricity, and makes your scrum master cry.
So why do we run irrelevant tests to begin with? Our testing habits came from unit tests, where the goal is to make tests so fast, you can rerun them all frequently. But this broke down as test suites got longer and slower.
I have personally seen CI for PRs take up to 120 minutes to complete, and full end-to-end tests so slow they could only be run overnight.
If your tests finish in under 15 minutes, great! âŚthis post isnât for you, close the tab.
What can we do instead of pulling our hair out?
Thereâs a solution that deserves to be better-known outside FAANGs: test-impact analysis (TIA). With TIA, you use code and file dependencies to skip tests that cannot be affected by changes in a PR.
TIA is not new. Variants of it are used in Googleâs TAP. Jest and Vitest support it. Microsoft coined the term itself, and offers it in Azure DevOps. Thought leaders have thought about it.
How does test-impact analysis work?
You might already have a pitchfork in hand, shouting âItâs unsafe to skip tests!â
But done correctly, TIA will only skip tests it knows are irrelevant. You can trust it as much as you trust your compiler (and some of the TIA methods are the same as your compilerâs).
Automatic
Automatic TIA methods use static code analysis or run-time coverage to figure out dependencies. You donât have to change anything with your tests for them to work. However, they donât usually understand non-code changes, so they run all tests to be safe in that case.
1. File dependencies
File dependencies are fast, safe, and simple, but can âovertestâ. Not every test in a file may be affected by a change in a depended-on code file. Itâs best for codebases that donât import/export more than they need to (e.g., Python __init__.py files that import everything under the sun.)
2. Program dependency graphs
Program dependency graphs are what compilers use in optimization, but they work for TIA, too. By analyzing data dependencies and control flow, they can match up tests to individual lines of code. They wonât overtest, though they still have to be cautious when non-code files change.
3. Coverage
Coverage can map code dependencies that arenât amenable to static analysis, like which branch of an if a test always takes.
This comes with a downside, though; to collect coverage info requires a full test run to start. If you want to use TIA in a fresh CI environment, you need to run all tests beforehand, store that coverage data, and share it with CI. (If you donât, CI has to run the full test suite, defeating the whole purpose.) Coverage methods work well, at the cost of more complexity, and harder CI integration.
Manual
In manual specification, you write out all the dependencies yourself. (This is more common in full build systems like Bazel.) The major advantage is you can use TIA for non-code changes.
But the major disadvantage is that unlike the automatic methods, which canât miss tests, if you specify the dependencies wrong, you can.
Complementary speed-up methods
TIA is safe, cheap, requires minimal changes, and combines well with other acceleration methods.
1. Test suites
The traditional speed-up approach divides tests into suites. When updating front-end-related code, run only the front-end suite. While simple, this method is coarse and needs manual setup.