A checkout function can have 100% line and branch coverage without a test for an expired session. Both return paths run. Both tests pass. But removing the expiry check still leaves the suite green.
MC/DC coverage asks a more specific question: has each condition been shown to change the decision on its own?
Here's what that looks like in JavaScript, using a small example we ran with Supercov.
The condition the tests miss
This function allows checkout when a customer is signed in and their session has not expired:
export function canCheckout(signedIn, expired) {
if (signedIn && !expired) return true;
return false;
}
The original tests check a valid session and a signed-out visitor:
assert.equal(canCheckout(true, false), true);
assert.equal(canCheckout(false, false), false);
One test takes the true return path. The other takes the false path. Neither passes true for expired.
If we accidentally change the condition to just signedIn, both tests still pass. An expired session would now be accepted.
What MC/DC measures
MC/DC stands for Modified Condition/Decision Coverage. A decision is the whole expression, signedIn && !expired. Its two conditions are signedIn and !expired.
For each condition, MC/DC looks for a pair of executions showing that the condition independently changes the decision's outcome. For this example, the useful cases are:
| Case | signedIn |
expired |
Checkout allowed? |
|---|---|---|---|
| Valid session | true |
false |
true |
| Signed out | false |
false |
false |
| Expired session | true |
true |
false |
Compare the first two rows: changing signedIn changes the result. Compare the first and third: changing expired changes the result while signedIn stays true. The original suite has the first pair, but not the second.
JavaScript short-circuits &&: when signedIn is false, it does not evaluate !expired. Coverage tools need to retain that distinction rather than treating a skipped condition as a false one. The precise witness rules also depend on the form of MC/DC being measured; Clang's coverage documentation explains its masking approach. For this small example, the missing expired-session case is straightforward.
Measure it with Supercov
Run your existing test command through Supercov, then query the result:
npx supercov -- npm test
npx supercov runs latest
Everything after -- is your project's test command. Here, npm test runs the example's two tests. This is the coverage summary from that recorded run:
Coverage
Lines 100.00% (3/3)
Branches 100.00% (2/2)
MC/DC 50.00% (1/2)
These are Supercov's counts for this file, not a claim that every coverage provider would print the same percentages. Different tools count branches differently. In particular, ordinary branch or condition counters are not the same as evidence that a condition independently changes a decision.
To see what's missing:
npx supercov runs latest gaps
npx supercov runs latest file src/session.js
The file view identifies the condition:
LINE STATUS SOURCE
2 PARTIAL signedIn && !expired
Unobserved: no witness pair shows `!expired` independently changing the decision result
That is enough information to choose a test. We need a signed-in customer whose session has expired.
Add a test that checks the missing behavior
In our recorded run, the coding agent added this test and left the application code unchanged:
test('a signed-in visitor with an expired session cannot check out', () => {
assert.equal(canCheckout(true, true), false);
});
The assertion matters. Calling the function would exercise the condition; checking the result verifies that this input is denied checkout.
After rerunning the same suite, all three tests passed. The agent compared the new run with the baseline:
npx supercov -- npm test
npx supercov diff <before-run-id> latest
Replace <before-run-id> with the ID from your baseline run. The recorded comparison showed:
lines +0pp, branches +0pp, MC/DC +50pp
gained: 0 lines, 0 branches, 1 MC/DC conditions
lost: 0 lines, 0 branches, 0 MC/DC conditions
+ MC/DC src/session.js:2 C2 !expired
Line and branch coverage did not change. The suite now checks a behavior it did not check before. Removing the expiry guard makes the new test fail.
You can download the original two-test project or inspect the source files and recorded run. The Agent workflow guide walks through reproducing it with your own agent.
Use it with a coding agent
You don't need to pick every gap yourself. Open your project in your agent and ask:
Measure code coverage with npx supercov and write one missing test.
Only change tests. Rerun the full test suite and show me the test you
added and the before-and-after coverage.
Supercov supplies the measurements. The agent runs the commands and writes the test. Review the changed test and the comparison in the conversation.
MC/DC is useful for conditions in session checks, permissions, discounts, and feature gates. It does not establish that the requirement itself is right, that every input is tested, or that the program is bug-free. Use it to find specific gaps, then write assertions for the behavior you actually want.
For runner and language requirements, see Supported languages. For the meaning and limits of each metric, see Understanding coverage.
Originally published at supercov.com.
Top comments (3)
Good concrete walkthrough. One thing I'm curious about, since the post itself flags that the witness rules depend on which form of MC/DC is being measured: which form does Supercov actually implement, masking or unique-cause? For a two-condition && it doesn't matter, but once you get to three or more conditions with some correlation between them, unique-cause MC/DC can be unsatisfiable while masking MC/DC still finds a witness pair, so a tool silently picking one changes what 100% actually certifies. Also, does the gaps view distinguish a condition that was evaluated and came out false from one that was never evaluated at all because of short-circuiting? Those feel like they'd want different messages in the file view.
Thanks! It's the same form Clang uses: other conditions have to stay the same, except ones skipped by short-circuiting. So it's a bit stricter than masking MC/DC. Idea was that this is mainly used by agents, so probably makes sense to give them as much verification as we can.
On short-circuts: yes, they're tracked separately.
npx supercov runs latest decision file.js:2shows skipped conditions as-. The file view doesn't say which case it is yet, so I'll add that in the next release.And if you have any other ideas how this could be improved, would be super grateful! Been working on code verification for a while, but there’s so much in the field haha :) Thanks Mihai!
Appreciate the full answer. The case-number gap in the file view is the one I'd have hit first too, since "a condition got skipped" and "which branch it was skipped in" are different questions when you're staring at a long decision expression. No other ideas off the top of my head, this is already more rigorous than most of what's in this space.