Coverage tells you what ran, not what mattered
Line coverage and a test selection map come from the same instrumentation and answer opposite questions. Confusing them is how teams end up skipping the wrong things.
Tomas Bergqvist · · 4 min read
Every few months somebody points out that we could have built this on top of the coverage report, since the coverage tool already records which lines executed. It is a fair question, and the answer is a useful way to explain what selection is.
Same instrumentation, opposite direction
A coverage report aggregates. It takes every test in the suite, unions what they touched, and hands you one number per file: this line was reached, that one was not. The aggregation is the point, because the question is “is any of this untested”.
A selection map refuses to aggregate. It keeps one entry per test, listing what that individual test loaded, and it never merges them. The question runs the other way: given this file, which tests can see it? You cannot recover that from a union. Once you have added the tests together you have thrown away the only thing selection needs.
You can build both from the same hooks. You cannot build one from the other.
The direction that bites
Suppose packages/core/money.ts is covered at 96 percent. Excellent. Now change it.
Which tests do you run?
The coverage report cannot answer. It knows the file was well exercised. It does not know by whom. The best you can do is run everything, which is where you started.
Now invert it. Suppose billing/tax_region_spec.rb loaded eleven files on its last
run and money.ts is not one of them. That is a fact about one test, recorded from one
real execution, and it is enough to leave that test out of this commit with a reason
you can print.
Two rules that keep it honest
The failure mode of selection is skipping something you should have run. It is a quiet failure, which makes it the one worth engineering against. Two rules do most of the work:
- A stale entry is not an answer. If a test’s map entry is older than the test’s own file, run it. The map records a past execution, and the test has changed since.
- A missing entry is not an answer either. A test that has never been mapped always runs. An unbuilt map and a map that says nothing is affected look identical, and the safe reading of both is the same.
Neither rule is clever. Together they are the difference between a tool that skips tests and a tool you can leave switched on.
The case where it does nothing
Change the base image and every test in the repository is downstream of the change. The map is not wrong and the tool is not failing. The correct selection is the whole suite and the run takes as long as it used to.
We show that case in the product demo rather than hide it, because a selection tool that reports a saving on a commit like that is not measuring your repository. It is measuring how much you wanted to hear a number.