{"id":"l-tj-2-1","courseId":"testing-javascript","lang":"en","kind":"lesson","summary":"Write tests that pin down a function’s contract (inputs, outputs and invariants) so they survive refactoring and fail only when behaviour really changes.","html":"<p>A test suite has two jobs: <strong>fail when behaviour breaks</strong>, and <strong>stay green when it doesn’t</strong>. Most teams focus on the first and forget the second. Then someone renames a private function, forty tests go red, nobody’s behaviour changed, and the lesson the team learns is “tests slow us down”.</p>\n<h2 id=\"sec-1\" tabindex=\"-1\">Behaviour versus implementation</h2>\n<p>Here is Splitwise-lite’s <code dir=\"ltr\">splitEvenly</code>, which divides a bill in cents between people. Leftover cents go to the first people in the list, so the result is fair (shares differ by at most one cent) and deterministic:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"js\" data-run=\"js\" data-index=\"0\"><div class=\"code-head\"><span class=\"code-title\">JavaScript</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn code-run\" data-action=\"run\" data-lang=\"js\" data-index=\"0\" aria-label=\"Run this example\"><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=\"M5 5a2 2 0 0 1 3.008-1.728l11.997 6.998a2 2 0 0 1 .003 3.458l-12 7A2 2 0 0 1 5 19z\"/></svg><span>Run</span></button><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:#CF222E;--shiki-dark:#FF7B72\">function</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> splitEvenly</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">totalCents</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#953800;--shiki-dark:#FFA657\">people</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) {</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  if</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">!</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">Number.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">isInteger</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(people) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">||</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> people </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">&#x3C;</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 1</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) {</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">    throw</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> new</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> RangeError</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'people must be a positive integer'</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:#CF222E;--shiki-dark:#FF7B72\">  const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> base</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> Math.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">floor</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(totalCents </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">/</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> people);</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> remainder</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> totalCents </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">%</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> people;</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  return</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> Array.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">from</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">({ length: people }, (</span><span style=\"color:#953800;--shiki-dark:#FFA657\">_</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#953800;--shiki-dark:#FFA657\">i</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (i </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">&#x3C;</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> remainder </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">?</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> base </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">+</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 1</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> :</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> base));</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">console.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">log</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">splitEvenly</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">1000</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 style=\"color:#6E7781;--shiki-dark:#8B949E\">// $10.00 three ways</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">console.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">log</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">splitEvenly</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">1001</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">4</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">));</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">console.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">log</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">splitEvenly</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">2</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></code></pre><div class=\"run-output\" data-run-output=\"0\" hidden></div></div>\n<p>Its <strong>behaviour</strong> is everything a caller can observe: the array it returns, the error it throws for zero people. Its <strong>implementation</strong> is how: <code dir=\"ltr\">Math.floor</code>, the <code dir=\"ltr\">%</code> operator, <code dir=\"ltr\">Array.from</code>, the variable names. You could rewrite the body with a loop, or by handing out cents one at a time, and no caller would notice.</p>\n<p>A test that checks behaviour survives all of those rewrites. A test that checks implementation breaks on every one of them, even though nothing a user cares about changed. That is a <em>false alarm</em>, and false alarms are expensive: each one costs a debugging session, and enough of them teach people to rerun or delete red tests without reading them.</p>\n<figure class=\"diagram\"><svg class=\"diagram-svg\" focusable=\"false\" viewBox=\"0 0 640 220\" role=\"img\" aria-labelledby=\"l-tj-2-1-t1\"><defs><marker id=\"arrow-l-tj-2-1-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-2-1-t1\">Tests send inputs to splitEvenly and check outputs; the internal helpers inside the dashed box are not touched by tests.</title>\n  <rect class=\"d-box-accent\" x=\"20\" y=\"70\" width=\"150\" height=\"80\" rx=\"12\"/>\n  <text class=\"d-label-strong\" x=\"95\" y=\"105\" text-anchor=\"middle\">Tests</text>\n  <text class=\"d-label-muted\" x=\"95\" y=\"128\" text-anchor=\"middle\">inputs, outputs</text>\n  <path class=\"d-arrow\" d=\"M170 95 L258 95\" marker-end=\"url(#arrow-l-tj-2-1-1)\"/>\n  <path class=\"d-arrow\" d=\"M258 128 L172 128\" marker-end=\"url(#arrow-l-tj-2-1-1)\"/>\n  <rect class=\"d-box-primary\" x=\"260\" y=\"30\" width=\"360\" height=\"160\" rx=\"14\"/>\n  <text class=\"d-label-strong\" x=\"300\" y=\"62\">splitEvenly(total, people)</text>\n  <rect class=\"d-box d-dashed\" x=\"290\" y=\"80\" width=\"300\" height=\"90\" rx=\"10\"/>\n  <text class=\"d-label-muted\" x=\"440\" y=\"112\" text-anchor=\"middle\">internals: floor, %, Array.from</text>\n  <text class=\"d-label-muted\" x=\"440\" y=\"140\" text-anchor=\"middle\">free to change</text>\n</svg><figcaption>Tests talk to the public contract, not the internals</figcaption></figure>\n<h2 id=\"sec-2\" tabindex=\"-1\">What implementation-coupled tests look like</h2>\n<p>These are real patterns I have deleted from codebases:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"js\" data-index=\"1\"><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:#6E7781;--shiki-dark:#8B949E\">// Spying on Math.floor: breaks if you switch to integer division another way</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> floor</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> vi.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">spyOn</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(Math, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'floor'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">splitEvenly</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">1000</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:#8250DF;--shiki-dark:#D2A8FF\">expect</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(floor).</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">toHaveBeenCalledWith</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">1000</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> /</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 3</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#6E7781;--shiki-dark:#8B949E\">// Testing an exported private helper: breaks when you inline or rename it</span></span>\n<span class=\"line\"><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">expect</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">_remainderFor</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">1000</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 style=\"color:#8250DF;--shiki-dark:#D2A8FF\">toBe</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">1</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#6E7781;--shiki-dark:#8B949E\">// Snapshotting the source-shaped output of a debug helper nobody calls</span></span>\n<span class=\"line\"><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">expect</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">describeSplit</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">1000</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 style=\"color:#8250DF;--shiki-dark:#D2A8FF\">toMatchSnapshot</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">();</span></span></code></pre></div>\n<p>Each one passes today and tells you nothing a behaviour test wouldn’t. Each one will fail the day someone improves the code.</p>\n<h2 id=\"sec-3\" tabindex=\"-1\">Testing the contract instead</h2>\n<p>Start from what a caller relies on. For <code dir=\"ltr\">splitEvenly</code> that is:</p>\n<ol>\n<li>One share per person.</li>\n<li><strong>The shares add up to the total.</strong> Not a cent lost, not a cent invented.</li>\n<li>Shares differ by at most one cent.</li>\n<li>The extra cents go to the first people (documented, so callers may depend on it).</li>\n<li>Zero people is an error.</li>\n</ol>\n<p>Points 2 and 3 are <em>invariants</em>: they hold for every valid input, so you can check them over several inputs at once:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"js\" data-index=\"2\"><div class=\"code-head\"><span class=\"code-title\">src/split.test.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:#CF222E;--shiki-dark:#FF7B72\">import</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { describe, test, expect } </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">from</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'vitest'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">import</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { splitEvenly } </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">from</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> './split.js'</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\">const</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> sum</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (</span><span style=\"color:#953800;--shiki-dark:#FFA657\">xs</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> xs.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">reduce</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">((</span><span style=\"color:#953800;--shiki-dark:#FFA657\">a</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#953800;--shiki-dark:#FFA657\">b</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> a </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">+</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> b, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">0</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">describe</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'splitEvenly'</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:#8250DF;--shiki-dark:#D2A8FF\">  test</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'never loses or invents a cent'</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\">    for</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">const</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> [</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">total</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">people</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">] </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">of</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> [[</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">1000</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 style=\"color:#0550AE;--shiki-dark:#79C0FF\">1001</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">4</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">], [</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">2</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 style=\"color:#0550AE;--shiki-dark:#79C0FF\">0</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">5</span><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\">(</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">sum</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">splitEvenly</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(total, people))).</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">toBe</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(total);</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>\n<span class=\"line\"></span>\n<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\">'gives the leftover cents to the first people'</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:#8250DF;--shiki-dark:#D2A8FF\">    expect</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">splitEvenly</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">1001</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">4</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">)).</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">toEqual</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">([</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">251</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">250</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">250</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">250</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>\n<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\">'refuses to split between zero people'</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:#8250DF;--shiki-dark:#D2A8FF\">    expect</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(() </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> splitEvenly</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">1000</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">0</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">)).</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">toThrow</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(RangeError);</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></code></pre></div>\n<p>The sum invariant is the most valuable line in the file. A version that drops the remainder fails it. So does an off-by-one that hands out one cent too many. It doesn’t care how the shares are computed, only that money is conserved.</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>Exporting internals to test them</span></p>\n<p>If a helper feels important enough to test on its own, ask what public behaviour depends on it, and test that with an input that exercises the helper. If the helper really is a separate concept used in several places, promote it to a real module with its own contract. “Exported only for tests” is the worst of both: public, but with no promises.</p>\n</aside>\n<h2 id=\"sec-4\" tabindex=\"-1\">How specific should assertions be?</h2>\n<p>Behaviour tests can still be exact. <code dir=\"ltr\">toEqual([251, 250, 250, 250])</code> pins the documented order of extra cents, and that is behaviour because callers rely on it. The question is never “exact or loose?” but “would a user or caller notice if this changed?” If yes, assert it precisely. If no, don’t assert it at all.</p>\n<h2 id=\"sec-5\" tabindex=\"-1\">When a refactor does break a test</h2>\n<p>Sometimes a change to the code <em>should</em> change a test, and it helps to know which situation you are in. If the public contract changed (the function now returns objects instead of numbers, or the extra cents now go to the payer), the test was right to fail, and you update it on purpose, ideally before changing the code. If the contract is the same and a test fails anyway, the test was coupled to the implementation: rewrite it against the contract and delete the old one. A healthy suite makes that question easy to answer, because every test name states a promise the code makes.</p>\n<p>In the exercise, write behaviour tests for <code dir=\"ltr\">splitEvenly</code> that catch four realistic bugs. The next lesson is about finding the inputs that expose bugs like these.</p>\n","toc":[{"id":"sec-1","text":"Behaviour versus implementation","level":2},{"id":"sec-2","text":"What implementation-coupled tests look like","level":2},{"id":"sec-3","text":"Testing the contract instead","level":2},{"id":"sec-4","text":"How specific should assertions be?","level":2},{"id":"sec-5","text":"When a refactor does break a test","level":2}],"takeaways":["Behaviour is what callers can observe (return values, thrown errors, visible side effects); implementation is how the code gets there.","A test that breaks when you refactor without changing behaviour is a false alarm, and false alarms teach a team to ignore red builds.","Invariants such as “the shares add up to the total” catch whole families of bugs with one assertion.","Don’t export private helpers only to test them; test them through the public function that uses them."],"further":[{"title":"Testing Library — Guiding principles","url":"https://testing-library.com/docs/guiding-principles"},{"title":"Vitest — expect API","url":"https://vitest.dev/api/expect"}],"quiz":[{"q":"<p>You rewrite <code dir=\"ltr\">splitEvenly</code> from <code dir=\"ltr\">Array.from</code> to a plain <code dir=\"ltr\">for</code> loop. Its outputs are identical for every input. Two tests go red. What does that tell you about those tests?</p>","options":[{"text":"They were testing implementation details, not behaviour.","why":"Correct. If behaviour didn’t change but tests failed, the tests were coupled to how the code works, which makes every refactor expensive."},{"text":"The refactor introduced a bug the other tests missed.","why":"The premise is that outputs are identical for every input; a test that fails anyway is reporting on structure, not a bug."},{"text":"The tests are correct and the loop should be reverted.","why":"Reverting a harmless refactor to please a test is the tail wagging the dog; fix the tests to check outputs instead."}],"answer":0},{"q":"<p>Which single assertion would catch the most different bugs in <code dir=\"ltr\">splitEvenly(totalCents, people)</code>?</p>","options":[{"text":"<code dir=\"ltr\">expect(shares).toHaveLength(people)</code>","why":"It is useful, but a version that loses the leftover cents still returns the right number of shares."},{"text":"<code dir=\"ltr\">expect(Math.max(...shares) - Math.min(...shares)).toBeLessThanOrEqual(1)</code>","why":"It catches unfair splits, but a version that drops the remainder gives everyone the same share and passes it."},{"text":"<code dir=\"ltr\">expect(shares.reduce((a, b) =&gt; a + b, 0)).toBe(totalCents)</code>","why":"Correct. Losing a cent, inventing a cent and mishandling the remainder all break the sum. It is the invariant money code must never violate."}],"answer":2},{"q":"<p>A teammate exports <code dir=\"ltr\">_remainderFor(total, people)</code> from <code dir=\"ltr\">split.js</code> so it can be unit tested directly. What’s the better approach?</p>","options":[{"text":"Keep the export but prefix it with an underscore so people know it’s private.","why":"An underscore doesn’t stop imports; the helper is now public API that tests (and other code) will couple to."},{"text":"Test the remainder behaviour through <code dir=\"ltr\">splitEvenly</code>, using inputs that leave a remainder.","why":"Correct. The helper only matters because of what <code dir=\"ltr\">splitEvenly</code> returns, so testing through the public function covers it and leaves you free to delete or rename the helper."},{"text":"Copy the helper into the test file and test the copy.","why":"Then the test checks a copy that can drift from the real code; it would stay green while the real helper breaks."}],"answer":1}],"exercise":{"kind":"web","mode":"test","subject":"// Splits a bill in integer cents between people.\n// Contract:\n//   - one share per person\n//   - the shares add up exactly to totalCents\n//   - shares differ by at most one cent\n//   - leftover cents go to the FIRST people in the list\n//   - fewer than one person throws a RangeError\nfunction 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","mutants":[{"id":"drops-remainder","code":"// Splits a bill in integer cents between people.\n// Contract:\n//   - one share per person\n//   - the shares add up exactly to totalCents\n//   - shares differ by at most one cent\n//   - leftover cents go to the FIRST people in the list\n//   - fewer than one person throws a RangeError\nfunction 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) => (base));\n}\n"},{"id":"extra-cent","code":"// Splits a bill in integer cents between people.\n// Contract:\n//   - one share per person\n//   - the shares add up exactly to totalCents\n//   - shares differ by at most one cent\n//   - leftover cents go to the FIRST people in the list\n//   - fewer than one person throws a RangeError\nfunction 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"},{"id":"remainder-last","code":"// Splits a bill in integer cents between people.\n// Contract:\n//   - one share per person\n//   - the shares add up exactly to totalCents\n//   - shares differ by at most one cent\n//   - leftover cents go to the FIRST people in the list\n//   - fewer than one person throws a RangeError\nfunction 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 >= people - remainder ? base + 1 : base));\n}\n"},{"id":"zero-people","code":"// Splits a bill in integer cents between people.\n// Contract:\n//   - one share per person\n//   - the shares add up exactly to totalCents\n//   - shares differ by at most one cent\n//   - leftover cents go to the FIRST people in the list\n//   - fewer than one person throws a RangeError\nfunction splitEvenly(totalCents, people) {\n  if (!Number.isInteger(people) || people < 0) {\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"}],"starter":{"js":"const sum = (xs) => xs.reduce((a, b) => a + b, 0);\n\ndescribe('splitEvenly', () => {\n  test('returns one share per person', () => {\n    expect(splitEvenly(1000, 3)).toHaveLength(3);\n  });\n\n  // Test the rest of the contract in the subject's comment.\n});\n"},"solution":{"js":"const sum = (xs) => xs.reduce((a, b) => a + b, 0);\n\ndescribe('splitEvenly', () => {\n  test('returns one share per person', () => {\n    expect(splitEvenly(1000, 3)).toHaveLength(3);\n  });\n\n  test('never loses or invents a cent', () => {\n    for (const [total, people] of [[1000, 3], [1001, 4], [2, 3], [900, 3], [0, 4]]) {\n      expect(sum(splitEvenly(total, people))).toBe(total);\n    }\n  });\n\n  test('keeps shares within one cent of each other', () => {\n    const shares = splitEvenly(1001, 4);\n    expect(Math.max(...shares) - Math.min(...shares)).toBeLessThanOrEqual(1);\n  });\n\n  test('gives the leftover cents to the first people', () => {\n    expect(splitEvenly(1001, 4)).toEqual([251, 250, 250, 250]);\n    expect(splitEvenly(1000, 3)).toEqual([334, 333, 333]);\n  });\n\n  test('refuses to split between zero people', () => {\n    expect(() => splitEvenly(1000, 0)).toThrow(RangeError);\n  });\n});\n"},"checks":[{"id":"passes","suite":"subject"},{"id":"catches-drops-remainder","suite":"drops-remainder"},{"id":"catches-extra-cent","suite":"extra-cent"},{"id":"catches-remainder-last","suite":"remainder-last"},{"id":"catches-zero-people","suite":"zero-people"}],"title":"Behaviour tests for splitEvenly","prompt":"<p>The subject’s comment lists <code dir=\"ltr\">splitEvenly</code>’s contract. Write tests for that contract (not for how the function is written) until your suite passes on the real code and catches four hidden bugs:</p>\n<ul>\n<li>one drops the leftover cents,</li>\n<li>one hands out one leftover cent too many,</li>\n<li>one gives the leftover cents to the <strong>last</strong> people instead of the first,</li>\n<li>one returns an empty array for zero people instead of throwing.</li>\n</ul>\n<p>A <code dir=\"ltr\">sum</code> helper is already in the starter.</p>\n","hints":["Inputs that divide evenly (like 900 between 3) hide every remainder bug. Use totals that leave a remainder, such as 1000 between 3 or 1001 between 4.","The sum invariant catches two of the bugs at once: <code dir=\"ltr\">expect(sum(splitEvenly(1000, 3))).toBe(1000)</code>.","For the order rule, compare the whole array: <code dir=\"ltr\">splitEvenly(1001, 4)</code> must equal <code dir=\"ltr\">[251, 250, 250, 250]</code>. For zero people, use <code dir=\"ltr\">expect(() =&gt; splitEvenly(1000, 0)).toThrow(RangeError)</code>."],"explanation":"<p>The sum invariant does the heavy lifting: dropping the remainder makes the total too small and the off-by-one makes it too big, so one assertion kills both. The order rule is part of the documented contract, so asserting the exact array is a behaviour test, not an implementation test; it is the only way to catch the version that favours the last people, which still conserves money and stays fair. The zero-people case is a boundary that silently returned <code dir=\"ltr\">[]</code> in the buggy version, and a caller would have divided a bill between nobody. None of these tests mention <code dir=\"ltr\">Math.floor</code>, <code dir=\"ltr\">%</code> or <code dir=\"ltr\">Array.from</code>, so you can rewrite the function freely and they stay green.</p>\n","checkLabels":{"passes":"All your tests pass against the real splitEvenly","catches-drops-remainder":"Catches the version that drops leftover cents","catches-extra-cent":"Catches the version that hands out one cent too many","catches-remainder-last":"Catches the version that gives leftover cents to the last people","catches-zero-people":"Catches the version that accepts zero people"}},"words":649,"readMinutes":3}