Skip to Content
Course content

128: Code Coverage Reporting

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I've seen a lot of developers treat the code coverage report as a scoreboard. You run npm test -- --coverage, see a 92% in the terminal, and feel a surge of confidence. Or worse, you're in a company with a strict "100% coverage or the PR is rejected" policy. When that happens, the goal shifts from "making the software robust" to "making the numbers turn green." This is where we run into trouble.

The Trap of the 100% Metric

The naive approach to coverage is treating it as a proxy for quality. Imagine you've written a function that handles complex shipping discounts based on user tier, order volume, and geographic region. It's a mess of if/else blocks. To hit 100% coverage, you might write a test that passes in a valid user and a valid order, which triggers the "success" path. Then, you write one test that passes a null value to trigger the "error" path.

// The kind of function we're talking about
function calculateShipping(user, order) {
  if (!user || !order) return 0; // Path A
  if (user.isPremium && order.total > 100) return 0; // Path B
  if (user.region === 'International') return 50; // Path C
  return 10; // Path D
}

If you write four tests that simply execute those paths, your coverage report will scream "100%!" You've touched every line. But did you actually test the logic? You might have checked that the function doesn't crash, but you didn't check if a premium user with a $101 order actually gets free shipping, or if an international premium user gets the $50 rate or the $0 rate. You've achieved "Line Coverage," but you have zero "Logic Coverage." I've seen entire systems crash in production despite having 100% line coverage because the tests were written to satisfy the tool, not to challenge the code.

Using Coverage to Find the Blind Spots

The better way to use coverage is as a "gap analysis" tool. Instead of starting with the report, start by writing tests based on the requirements and the edges of your logic. Once those are passing, then you run the coverage report. You aren't looking for a high number; you're looking for the red lines.

When I see a red line in a coverage report, I don't immediately think, "I need to write a test to cover that." Instead, I ask, "Why is this code here? Is there a scenario I forgot to consider?" If the report shows that a specific catch block is never hit, it's a signal. Maybe that error is actually impossible to trigger, meaning I've written dead code that should be deleted. Or, more likely, I've discovered a failure mode—like a network timeout or a database disconnection—that I completely ignored in my test suite. In this workflow, the coverage report is a prompt for critical thinking, not a checklist for completion.

Where the Numbers Lie

The trade-off here is between speed and certainty. Chasing 100% is fast because it's a mechanical process; you just keep adding inputs until the red goes away. Thinking through the "why" of the uncovered lines is slower and more mentally taxing. But it's the only way to actually prevent regressions.

Keep in mind that "Branch Coverage" is far more valuable than "Line Coverage." Line coverage tells you if the engine started; branch coverage tells you if the car can actually turn left and right. If you have a complex ternary operator on one line, line coverage will mark it as "covered" the moment either side of the condition is hit. Branch coverage will tell you that you've only tested the true case and the false case is still a mystery. If you're going to trust any number in your report, make it the branch percentage.




📋 Practical Task

Audit and Fill the Gaps in the Order Validator

You have been handed a validateOrder function and a corresponding test file. The current test suite is designed to hit 100% line coverage, but it ignores several critical edge cases (logic gaps).

// orderValidator.js
export function validateOrder(order) {
  if (!order) return { valid: false, error: 'No order provided' };
  if (order.items && order.items.length === 0) return { valid: false, error: 'Empty cart' };
  if (order.total < 0) return { valid: false, error: 'Invalid total' };
  if (order.customer && order.customer.isBlacklisted) return { valid: false, error: 'Customer blocked' };
  
  return { valid: true };
}
// orderValidator.test.js
test('coverage check', () => {
  expect(validateOrder(null)).toEqual({ valid: false, error: 'No order provided' });
  expect(validateOrder({ items: [] })).toEqual({ valid: false, error: 'Empty cart' });
  expect(validateOrder({ items: [1], total: -1 })).toEqual({ valid: false, error: 'Invalid total' });
  expect(validateOrder({ items: [1], total: 10, customer: { isBlacklisted: true } })).toEqual({ valid: false, error: 'Customer blocked' });
  expect(validateOrder({ items: [1], total: 10, customer: { isBlacklisted: false } })).toEqual({ valid: true });
});

Your Task: The current test hits 100% line coverage but is fragile. Refactor the tests to move away from a single "coverage check" block. Add specific test cases that challenge the logic, specifically addressing:

  1. What happens if order.items is undefined? (The current code handles this, but is it explicitly tested?)
  2. What happens if order.customer is missing? (Does the code crash when checking isBlacklisted?)
  3. Ensure each validation rule is tested in its own test()` or it()` block to ensure a failure in one doesn't mask a failure in another.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.