{"id":"l-tj-5-2","courseId":"testing-javascript","lang":"en","kind":"lesson","summary":"Measure code coverage with Vitest, read the report to find untested code, and explain why a high coverage number can still hide a weak test suite.","html":"<p>Every team that adds tests eventually asks “how much is enough?”, and coverage is the number that seems to answer. It answers a narrower question than people think. Used well, it’s a fast way to find code nobody tested. Used as a score, it rewards the wrong tests.</p>\n<h2 id=\"sec-1\" tabindex=\"-1\">Turning it on</h2>\n<p>Vitest supports two coverage providers. The default, <code dir=\"ltr\">v8</code>, uses the JavaScript engine’s built-in coverage and needs no instrumentation step; <code dir=\"ltr\">istanbul</code> instruments the code and works in more runtimes. Install the provider and run with <code dir=\"ltr\">--coverage</code>:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"bash\" data-index=\"0\"><div class=\"code-head\"><span class=\"code-title\">Terminal</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#953800;--shiki-dark:#FFA657\">npm</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> install</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> --save-dev</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> @vitest/coverage-v8</span></span>\n<span class=\"line\"><span style=\"color:#953800;--shiki-dark:#FFA657\">npx</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> vitest</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> run</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> --coverage</span></span></code></pre></div>\n<p>Configure what to measure and how to report it:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"js\" data-index=\"1\"><div class=\"code-head\"><span class=\"code-title\">vite.config.js</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#6E7781;--shiki-dark:#8B949E\">/// &#x3C;</span><span style=\"color:#116329;--shiki-dark:#7EE787\">reference</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> types</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"vitest/config\"</span><span style=\"color:#6E7781;--shiki-dark:#8B949E\"> /></span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">import</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { defineConfig } </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">from</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'vite'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">export</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> default</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> defineConfig</span><span style=\"color:#953800;--shiki-dark:#FFA657\">({</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  test: {</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    coverage: {</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">      provider: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'v8'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">,</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">      include: [</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'src/**/*.{js,jsx}'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">],</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">      exclude: [</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'src/**/*.test.{js,jsx}'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'src/main.jsx'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">],</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">      reporter: [</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'text'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'html'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">],</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">      thresholds: { lines: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">80</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, branches: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">75</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> },</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    },</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  }</span><span style=\"color:#953800;--shiki-dark:#FFA657\">,</span></span>\n<span class=\"line\"><span style=\"color:#953800;--shiki-dark:#FFA657\">})</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span></code></pre></div>\n<p><code dir=\"ltr\">include</code> matters: in Vitest 4, without it, the report lists only files your tests happened to load, so a completely untested module doesn’t appear at all and the percentage looks better than it is. With <code dir=\"ltr\">include</code>, untested files show up at 0%. The <code dir=\"ltr\">html</code> reporter writes <code dir=\"ltr\">coverage/index.html</code>, which you open in a browser.</p>\n<h2 id=\"sec-2\" tabindex=\"-1\">The four numbers</h2>\n<ul>\n<li><strong>Statements</strong>: how many statements ran.</li>\n<li><strong>Branches</strong>: how many sides of each decision ran (<code dir=\"ltr\">if</code>/<code dir=\"ltr\">else</code>, <code dir=\"ltr\">? :</code>, <code dir=\"ltr\">&amp;&amp;</code>, <code dir=\"ltr\">||</code>, <code dir=\"ltr\">??</code>, default parameters).</li>\n<li><strong>Functions</strong>: how many functions were called at least once.</li>\n<li><strong>Lines</strong>: how many lines had something run on them.</li>\n</ul>\n<p>Branch coverage is the one I look at. <code dir=\"ltr\">const share = people &gt; 0 ? total / people : 0;</code> is one line and two branches. A test with three people makes the line 100% covered and the branches 50%, and the untested branch is exactly the edge case from section 2.</p>\n<h2 id=\"sec-3\" tabindex=\"-1\">Reading the report</h2>\n<p>The HTML report marks lines that never ran in red and branches that were taken only one way in yellow. That’s the valuable part: <strong>red code is untested, guaranteed.</strong> No assertion anywhere could have checked it, because it never ran. When I join a project, the coverage report is the fastest map of where the risk lives: the uncovered error handling in the payment module, the <code dir=\"ltr\">else</code> branch nobody hit.</p>\n<figure class=\"diagram\"><svg class=\"diagram-svg\" focusable=\"false\" viewBox=\"0 0 640 230\" role=\"img\" aria-labelledby=\"l-tj-5-2-t1\"><defs><marker id=\"arrow-l-tj-5-2-1\" viewBox=\"0 0 10 10\" refX=\"9\" refY=\"5\" markerWidth=\"7\" markerHeight=\"7\" orient=\"auto-start-reverse\"><path d=\"M0 0 L10 5 L0 10 z\" class=\"d-arrowhead\"/></marker></defs>\n  <title id=\"l-tj-5-2-t1\">A large box of all code; inside it a box of code executed by tests; inside that a smaller box of code whose results tests actually verify. Coverage reports the middle box.</title>\n  <rect class=\"d-box\" x=\"20\" y=\"16\" width=\"600\" height=\"200\" rx=\"16\"/>\n  <text class=\"d-label-strong\" x=\"40\" y=\"44\">All code</text>\n  <rect class=\"d-box-accent\" x=\"120\" y=\"60\" width=\"440\" height=\"140\" rx=\"14\"/>\n  <text class=\"d-label-strong\" x=\"140\" y=\"88\">Executed by tests</text>\n  <text class=\"d-label-muted\" x=\"400\" y=\"88\">what coverage reports</text>\n  <rect class=\"d-box-success\" x=\"200\" y=\"106\" width=\"250\" height=\"78\" rx=\"12\"/>\n  <text class=\"d-label-strong\" x=\"325\" y=\"140\" text-anchor=\"middle\">Results verified</text>\n  <text class=\"d-label-muted\" x=\"325\" y=\"164\" text-anchor=\"middle\">what you need</text>\n</svg><figcaption>Coverage sees what ran, not what was checked</figcaption></figure>\n<h2 id=\"sec-4\" tabindex=\"-1\">What coverage can’t see</h2>\n<p>Coverage records <strong>execution</strong>. It has no idea whether anything checked the result. This test makes <code dir=\"ltr\">computeBalances</code> 100% covered:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"js\" data-index=\"2\"><div class=\"code-head\"><span class=\"code-title\">JavaScript</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">test</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'computes balances'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, () </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> balances</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> computeBalances</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">([</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    { paidBy: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'Ana'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, amountCents: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">3000</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, splitAmong: [</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'Ana'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'Ben'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'Cai'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">] },</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  ]);</span></span>\n<span class=\"line\"><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">  expect</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(Object.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">keys</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(balances)).</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">toHaveLength</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">3</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">});</span></span></code></pre></div>\n<p>Every line ran. The assertion would still pass if the payer were debited instead of credited, if the shares were floored and a cent vanished, or if a second expense overwrote the first. Coverage: 100%. Confidence: close to zero. The exercise starts from exactly this test.</p>\n<aside class=\"callout callout-mistake\" role=\"note\"><p class=\"callout-title\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><path d=\"m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3\"/><path d=\"M12 9v4\"/><path d=\"M12 17h.01\"/></svg><span><span class=\"callout-kind\">Common mistake</span><span class=\"callout-sep\" aria-hidden=\"true\"> · </span>Coverage as a target</span></p>\n<p>When a team is measured on the number, the number goes up, and the easiest way up is tests like the one above: call everything, assert little. That’s Goodhart’s law: a measure that becomes a target stops being a good measure. Set a threshold slightly below where you are today, as a <strong>floor</strong> that stops coverage from sliding when someone adds untested code, and never as a goal to chase.</p>\n</aside>\n<p>An uncovered line is a question, not an order. Sometimes the right answer is a test. Sometimes it’s deleting the code: a branch nobody can reach, a fallback for a browser you stopped supporting, a helper nothing calls any more. Dead code that is “covered” by a test written only to raise the number is the worst outcome, because now it looks important.</p>\n<h2 id=\"sec-5\" tabindex=\"-1\">How much is enough?</h2>\n<p>There’s no universal number. Useful rules of thumb:</p>\n<ul>\n<li>Aim for <strong>high branch coverage on core logic</strong> (money, permissions, data transformations), and accept less on glue code and UI wiring that end-to-end tests exercise anyway.</li>\n<li>Exclude code you deliberately don’t unit test (entry points, generated files, type-only modules) so the number means something.</li>\n<li>Look at coverage <strong>on the diff</strong> in code review: did this change add untested branches? That’s more actionable than a project-wide percentage.</li>\n</ul>\n<p>Coverage tells you where tests are missing. To learn whether the tests you have are any good, you need a different tool, which is the next lesson. First, in the exercise, turn a 100%-coverage, catches-nothing suite into one that catches real bugs.</p>\n","toc":[{"id":"sec-1","text":"Turning it on","level":2},{"id":"sec-2","text":"The four numbers","level":2},{"id":"sec-3","text":"Reading the report","level":2},{"id":"sec-4","text":"What coverage can’t see","level":2},{"id":"sec-5","text":"How much is enough?","level":2}],"takeaways":["Coverage measures which statements, branches, functions and lines ran during the tests, not whether anything checked the results.","The most useful part of a coverage report is the red and yellow it shows you; uncovered code is untested code for certain.","Branch coverage is stricter and more useful than line coverage, because one line can hold several decisions.","Use thresholds as a floor that stops coverage from sliding, not as a target to hit; a number that becomes a goal gets gamed."],"further":[{"title":"Vitest — Coverage guide","url":"https://vitest.dev/guide/coverage"},{"title":"Vitest — Coverage config","url":"https://vitest.dev/config/coverage"}],"quiz":[{"q":"<p>A test calls <code dir=\"ltr\">computeBalances(expenses)</code> and asserts only <code dir=\"ltr\">expect(result).toBeDefined()</code>. Coverage for the file is 100%. What does that tell you?</p>","options":[{"text":"The function is fully tested.","why":"Every line ran, but nothing checked the numbers; a sign error would still pass."},{"text":"Every line ran during the tests, and nothing about whether the results are right.","why":"Correct. Coverage measures execution. The quality of the assertions is invisible to it."},{"text":"Vitest miscounted, because 100% is impossible with one test.","why":"One test can easily execute every line of a small function; that’s exactly why coverage alone is a weak signal."}],"answer":1},{"q":"<p>Line coverage is 100% but branch coverage is 50% for <code dir=\"ltr\">const share = people &gt; 0 ? total / people : 0;</code>. What’s missing?</p>","options":[{"text":"A test where <code dir=\"ltr\">people</code> is 0 (or negative), so the other side of the conditional runs.","why":"Correct. The line ran, but only one of its two branches did; branch coverage notices, line coverage can’t."},{"text":"A second test with a larger <code dir=\"ltr\">total</code>.","why":"A different total takes the same branch, so branch coverage stays at 50%."},{"text":"Nothing; branch coverage is always lower than line coverage.","why":"It’s often lower, but here it is pointing at a specific untested decision."}],"answer":0},{"q":"<p>Your team sets a hard 90% coverage threshold and makes it a quarterly goal. What is the most likely side effect?</p>","options":[{"text":"Fewer bugs, because 90% of the code is now verified.","why":"Coverage counts execution, not verification, so the number can rise without bugs falling."},{"text":"Slower CI, because coverage is expensive to collect.","why":"There’s some overhead, but that’s not the main risk of turning the number into a goal."},{"text":"People write tests that execute code without meaningful assertions, so the number rises while confidence doesn’t.","why":"Correct. Once a measure becomes a target, it gets optimised directly; use thresholds as a floor and read the report for gaps instead."}],"answer":2}],"exercise":{"kind":"web","mode":"test","subject":"function splitEvenly(totalCents, people) {\n  if (!Number.isInteger(people) || people < 1) {\n    throw new RangeError('people must be a positive integer');\n  }\n  const base = Math.floor(totalCents / people);\n  const remainder = totalCents % people;\n  return Array.from({ length: people }, (_, i) => (i < remainder ? base + 1 : base));\n}\n\n// Computes each person's balance in cents from a list of expenses.\n// Positive: the group owes them. Negative: they owe the group.\n// Each expense is { paidBy, amountCents, splitAmong: [names] }.\n// - The payer is credited the full amount.\n// - Each person in splitAmong is debited their share from splitEvenly\n//   (leftover cents go to the first names in the list).\n// - Balances accumulate across expenses and always add up to zero.\nfunction computeBalances(expenses) {\n  const balances = {};\n  for (const { paidBy, amountCents, splitAmong } of expenses) {\n    const shares = splitEvenly(amountCents, splitAmong.length);\n    balances[paidBy] = (balances[paidBy] ?? 0) + amountCents;\n    splitAmong.forEach((name, i) => {\n      balances[name] = (balances[name] ?? 0) - shares[i];\n    });\n  }\n  return balances;\n}\n","mutants":[{"id":"payer-debited","code":"function splitEvenly(totalCents, people) {\n  if (!Number.isInteger(people) || people < 1) {\n    throw new RangeError('people must be a positive integer');\n  }\n  const base = Math.floor(totalCents / people);\n  const remainder = totalCents % people;\n  return Array.from({ length: people }, (_, i) => (i < remainder ? base + 1 : base));\n}\n\n// Computes each person's balance in cents from a list of expenses.\n// Positive: the group owes them. Negative: they owe the group.\n// Each expense is { paidBy, amountCents, splitAmong: [names] }.\n// - The payer is credited the full amount.\n// - Each person in splitAmong is debited their share from splitEvenly\n//   (leftover cents go to the first names in the list).\n// - Balances accumulate across expenses and always add up to zero.\nfunction computeBalances(expenses) {\n  const balances = {};\n  for (const { paidBy, amountCents, splitAmong } of expenses) {\n    const shares = splitEvenly(amountCents, splitAmong.length);\n    balances[paidBy] = (balances[paidBy] ?? 0) - amountCents;\n    splitAmong.forEach((name, i) => {\n      balances[name] = (balances[name] ?? 0) - shares[i];\n    });\n  }\n  return balances;\n}\n"},{"id":"floored-shares","code":"function splitEvenly(totalCents, people) {\n  if (!Number.isInteger(people) || people < 1) {\n    throw new RangeError('people must be a positive integer');\n  }\n  const base = Math.floor(totalCents / people);\n  const remainder = totalCents % people;\n  return Array.from({ length: people }, (_, i) => (i < remainder ? base + 1 : base));\n}\n\n// Computes each person's balance in cents from a list of expenses.\n// Positive: the group owes them. Negative: they owe the group.\n// Each expense is { paidBy, amountCents, splitAmong: [names] }.\n// - The payer is credited the full amount.\n// - Each person in splitAmong is debited their share from splitEvenly\n//   (leftover cents go to the first names in the list).\n// - Balances accumulate across expenses and always add up to zero.\nfunction computeBalances(expenses) {\n  const balances = {};\n  for (const { paidBy, amountCents, splitAmong } of expenses) {\n    const shares = splitEvenly(amountCents, splitAmong.length);\n    balances[paidBy] = (balances[paidBy] ?? 0) + amountCents;\n    splitAmong.forEach((name, i) => {\n      balances[name] = (balances[name] ?? 0) - Math.floor(amountCents / splitAmong.length);\n    });\n  }\n  return balances;\n}\n"},{"id":"overwrites-payer","code":"function splitEvenly(totalCents, people) {\n  if (!Number.isInteger(people) || people < 1) {\n    throw new RangeError('people must be a positive integer');\n  }\n  const base = Math.floor(totalCents / people);\n  const remainder = totalCents % people;\n  return Array.from({ length: people }, (_, i) => (i < remainder ? base + 1 : base));\n}\n\n// Computes each person's balance in cents from a list of expenses.\n// Positive: the group owes them. Negative: they owe the group.\n// Each expense is { paidBy, amountCents, splitAmong: [names] }.\n// - The payer is credited the full amount.\n// - Each person in splitAmong is debited their share from splitEvenly\n//   (leftover cents go to the first names in the list).\n// - Balances accumulate across expenses and always add up to zero.\nfunction computeBalances(expenses) {\n  const balances = {};\n  for (const { paidBy, amountCents, splitAmong } of expenses) {\n    const shares = splitEvenly(amountCents, splitAmong.length);\n    balances[paidBy] = amountCents;\n    splitAmong.forEach((name, i) => {\n      balances[name] = (balances[name] ?? 0) - shares[i];\n    });\n  }\n  return balances;\n}\n"}],"starter":{"js":"// This suite gives computeBalances 100% line coverage.\n// It also catches almost nothing. Strengthen it.\ndescribe('computeBalances', () => {\n  test('computes balances', () => {\n    const balances = computeBalances([\n      { paidBy: 'Ana', amountCents: 3000, splitAmong: ['Ana', 'Ben', 'Cai'] },\n    ]);\n\n    expect(Object.keys(balances)).toHaveLength(3);\n  });\n});\n"},"solution":{"js":"const sum = (balances) => Object.values(balances).reduce((a, b) => a + b, 0);\n\ndescribe('computeBalances', () => {\n  test('credits the payer and debits everyone their share', () => {\n    const balances = computeBalances([\n      { paidBy: 'Ana', amountCents: 3000, splitAmong: ['Ana', 'Ben', 'Cai'] },\n    ]);\n\n    expect(balances).toEqual({ Ana: 2000, Ben: -1000, Cai: -1000 });\n  });\n\n  test('balances add up to zero when the bill does not divide evenly', () => {\n    const balances = computeBalances([\n      { paidBy: 'Ana', amountCents: 1000, splitAmong: ['Ana', 'Ben', 'Cai'] },\n    ]);\n\n    expect(balances).toEqual({ Ana: 666, Ben: -333, Cai: -333 });\n    expect(sum(balances)).toBe(0);\n  });\n\n  test('accumulates several expenses paid by the same person', () => {\n    const balances = computeBalances([\n      { paidBy: 'Ana', amountCents: 2000, splitAmong: ['Ana', 'Ben'] },\n      { paidBy: 'Ana', amountCents: 1000, splitAmong: ['Ana', 'Ben'] },\n    ]);\n\n    expect(balances).toEqual({ Ana: 1500, Ben: -1500 });\n  });\n});\n"},"checks":[{"id":"passes","suite":"subject"},{"id":"catches-payer-debited","suite":"payer-debited"},{"id":"catches-floored-shares","suite":"floored-shares"},{"id":"catches-overwrites-payer","suite":"overwrites-payer"}],"title":"From 100% coverage to real confidence","prompt":"<p>The starter test runs every line of <code dir=\"ltr\">computeBalances</code> (and <code dir=\"ltr\">splitEvenly</code>), so a coverage report would show 100%. Yet it checks only that three names come back.</p>\n<p>Strengthen the suite until it catches three hidden bugs that the 100% suite misses: one <strong>debits</strong> the payer instead of crediting them, one <strong>floors</strong> each share so leftover cents disappear (balances no longer add up to zero), and one <strong>overwrites</strong> the payer’s balance instead of adding to it when they pay twice. The rules are in the subject’s comment.</p>\n","hints":["Replace the key-count assertion with the exact result: Ana pays 3000 split three ways, so <code dir=\"ltr\">{ Ana: 2000, Ben: -1000, Cai: -1000 }</code>.","Use a bill that does not divide evenly, such as 1000 between three people, and assert that the balances add up to 0.","Give the same payer two expenses and check that both are counted."],"explanation":"<p>Coverage stayed at 100% the whole time: your new tests ran no line the old one didn’t. What changed is <strong>what the tests check</strong>. The exact <code dir=\"ltr\">toEqual</code> on the first expense catches the sign error. A bill of 1000 cents between three people leaves a cent over, so floored shares make the balances add up to +1 instead of 0; the sum invariant from section 2 catches it. Two expenses from the same payer exercise accumulation, which a single expense never can, even though the accumulating line was already ‘covered’. That’s the gap between execution and verification, and it’s what mutation testing measures directly.</p>\n","checkLabels":{"passes":"All your tests pass against the real computeBalances","catches-payer-debited":"Catches the version that debits the payer","catches-floored-shares":"Catches the version that loses leftover cents","catches-overwrites-payer":"Catches the version that overwrites the payer’s balance"}},"words":647,"readMinutes":3}