{"id":"l-tj-5-1","courseId":"testing-javascript","lang":"en","kind":"lesson","summary":"Use the red-green-refactor cycle to build well-specified logic and to fix bugs, and recognise the situations where writing tests first slows you down.","html":"<p>Test-driven development has a reputation problem. Some people treat it as a religion; others dismiss it after one painful kata. I treat it as a tool, and like any tool it’s excellent for some jobs and wrong for others. For the core money logic in Splitwise-lite, it’s the best tool I know.</p>\n<h2 id=\"sec-1\" tabindex=\"-1\">The cycle</h2>\n<p>TDD is a short loop, usually a minute or two per turn:</p>\n<ol>\n<li><strong>Red</strong>: write one small test for behaviour that doesn’t exist yet. Run it. Watch it fail, ideally with the failure you expected.</li>\n<li><strong>Green</strong>: write the simplest code that makes it pass. Simple, not clever; hard-coding is allowed if it’s honest.</li>\n<li><strong>Refactor</strong>: clean up the code (and the tests) while everything stays green.</li>\n</ol>\n<figure class=\"diagram\"><svg class=\"diagram-svg\" focusable=\"false\" viewBox=\"0 0 640 260\" role=\"img\" aria-labelledby=\"l-tj-5-1-t1\"><defs><marker id=\"arrow-l-tj-5-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-5-1-t1\">A cycle of three boxes: red, write a failing test; green, make it pass; refactor, clean up with tests green; then back to red.</title>\n  <rect class=\"d-box-warn\" x=\"240\" y=\"16\" width=\"160\" height=\"66\" rx=\"14\"/>\n  <text class=\"d-label-strong\" x=\"320\" y=\"44\" text-anchor=\"middle\">Red</text>\n  <text class=\"d-label-muted\" x=\"320\" y=\"68\" text-anchor=\"middle\">a failing test</text>\n  <rect class=\"d-box-success\" x=\"430\" y=\"160\" width=\"180\" height=\"66\" rx=\"14\"/>\n  <text class=\"d-label-strong\" x=\"520\" y=\"188\" text-anchor=\"middle\">Green</text>\n  <text class=\"d-label-muted\" x=\"520\" y=\"212\" text-anchor=\"middle\">simplest passing code</text>\n  <rect class=\"d-box-primary\" x=\"30\" y=\"160\" width=\"180\" height=\"66\" rx=\"14\"/>\n  <text class=\"d-label-strong\" x=\"120\" y=\"188\" text-anchor=\"middle\">Refactor</text>\n  <text class=\"d-label-muted\" x=\"120\" y=\"212\" text-anchor=\"middle\">clean up, stay green</text>\n  <path class=\"d-arrow\" d=\"M400 60 Q500 80 515 155\" marker-end=\"url(#arrow-l-tj-5-1-1)\"/>\n  <path class=\"d-arrow\" d=\"M430 205 L214 205\" marker-end=\"url(#arrow-l-tj-5-1-1)\"/>\n  <path class=\"d-arrow\" d=\"M125 158 Q140 80 236 56\" marker-end=\"url(#arrow-l-tj-5-1-1)\"/>\n</svg><figcaption>Red, green, refactor</figcaption></figure>\n<p>Why insist on seeing red? Because a test you’ve never seen fail might not be able to fail. It might call the wrong function, assert on a variable that’s always truthy, or never actually check anything (a missing <code dir=\"ltr\">await</code>, a <code dir=\"ltr\">.skip</code> left in place). Red first is cheap proof that the test is wired up.</p>\n<h2 id=\"sec-2\" tabindex=\"-1\">Building settleUp test-first</h2>\n<p>Splitwise-lite needs <code dir=\"ltr\">settleUp(balances)</code>: given who is owed and who owes (in cents), return a short list of transfers that brings everyone to zero. Here is how the first few turns go, with Vitest in watch mode so every save reruns the tests.</p>\n<p><strong>Turn 1, the simplest case.</strong> Nobody owes anything:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"js\" data-index=\"0\"><div class=\"code-head\"><span class=\"code-title\">src/settle-up.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:#8250DF;--shiki-dark:#D2A8FF\">test</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'needs no transfers when everyone is settled'</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\">settleUp</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">({ Ana: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">0</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, Ben: </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\">toEqual</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>Red: <code dir=\"ltr\">settleUp is not defined</code>. Green: <code dir=\"ltr\">export function settleUp() { return []; }</code>. Yes, that’s the whole implementation for now. It’s honest about what the tests demand.</p>\n<p><strong>Turn 2, one debt.</strong> The next test forces real behaviour:</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:#8250DF;--shiki-dark:#D2A8FF\">test</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'the debtor pays the creditor'</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\">settleUp</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">({ Ana: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">500</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, Ben: </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">-</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">500</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>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    { from: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'Ben'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, to: </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\">500</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></code></pre></div>\n<p>Green might pair the one negative balance with the one positive balance. Still small.</p>\n<p><strong>Turn 3, several people.</strong> Now a hard-coded pairing breaks, and the general algorithm earns its place: sort debtors and creditors by amount, repeatedly move the smaller of the two current amounts, advance whichever side reaches zero. For assertions, use <strong>properties</strong> where several answers are valid: applying the transfers brings every balance to zero, every amount is positive, and there are at most <code dir=\"ltr\">people - 1</code> transfers.</p>\n<p><strong>Turn 4, invalid input.</strong> Balances that don’t add up to zero mean a bug upstream; <code dir=\"ltr\">settleUp</code> should refuse rather than invent money: <code dir=\"ltr\">expect(() =&gt; settleUp({ Ana: 500, Ben: -400 })).toThrow()</code>.</p>\n<p>Then <strong>refactor</strong>: name the sort comparator, extract a helper, make it read well. The tests let you change the shape of the code without fear, which is the real payoff.</p>\n<aside class=\"callout callout-tip\" 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=\"M15 14c.2-1 .7-1.7 1.5-2.5 1-.9 1.5-2.2 1.5-3.5A6 6 0 0 0 6 8c0 1 .2 2.2 1.5 3.5.7.7 1.3 1.5 1.5 2.5\"/><path d=\"M9 18h6\"/><path d=\"M10 22h4\"/></svg><span><span class=\"callout-kind\">Tip</span><span class=\"callout-sep\" aria-hidden=\"true\"> · </span>Choosing the next test</span></p>\n<p>Pick the test that forces the <strong>smallest</strong> step of new behaviour. If a test would make you write the whole algorithm at once, find a simpler one first. If you can’t think of a failing test, you might be done.</p>\n</aside>\n<h2 id=\"sec-3\" tabindex=\"-1\">Fixing bugs test-first</h2>\n<p>The most valuable everyday use of TDD has nothing to do with new features. When a bug is reported:</p>\n<ol>\n<li>Write a test that reproduces it, and <strong>watch it fail</strong>. Now you know you understand the bug.</li>\n<li>Fix the code until the test passes.</li>\n<li>Keep the test. That bug can never come back silently.</li>\n</ol>\n<p>This works even in codebases that were never written test-first, and it turns every bug report into a permanent improvement of the suite.</p>\n<h2 id=\"sec-4\" tabindex=\"-1\">When not to</h2>\n<p>TDD assumes you know what “correct” means. When you don’t, writing tests first just writes down guesses:</p>\n<ul>\n<li><strong>Exploratory UI work</strong>: trying layouts, interactions and copy. Build it, look at it, show it to someone; test what survives.</li>\n<li><strong>Spikes</strong>: a throwaway experiment to learn whether an approach works. Delete it, then build the real thing (test-first if you like).</li>\n<li><strong>Glue code with no logic</strong>: wiring a library’s callback to your state. Cover it with an integration or end-to-end test instead.</li>\n</ul>\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>Writing many tests before any code</span></p>\n<p>TDD is one test at a time. Writing twenty failing tests up front is planning, not TDD: you lose the tight feedback of each red-green turn, and the first implementation has to satisfy everything at once, which is exactly the big leap TDD exists to avoid.</p>\n</aside>\n<p>In the exercise you play the green half of the cycle: the tests for <code dir=\"ltr\">settleUp</code> are written (they’re the checks), and you write the code that makes them pass. In the next two lessons, you’ll measure how good those tests are.</p>\n","toc":[{"id":"sec-1","text":"The cycle","level":2},{"id":"sec-2","text":"Building settleUp test-first","level":2},{"id":"sec-3","text":"Fixing bugs test-first","level":2},{"id":"sec-4","text":"When not to","level":2}],"takeaways":["Red, green, refactor: write one failing test, make it pass with the simplest code, then clean up while the tests stay green.","Seeing a test fail first proves it can fail; a test written after the code may pass for the wrong reason.","Choose the next test so it forces one small step of new behaviour, starting from the simplest case.","TDD shines for logic with clear rules and for bug fixes (reproduce with a failing test first); it fits poorly with exploratory UI work and throwaway spikes."],"further":[{"title":"Vitest — Watch mode and filtering","url":"https://vitest.dev/guide/filtering"},{"title":"Vitest — expect API","url":"https://vitest.dev/api/expect"}],"quiz":[{"q":"<p>You write a test for a new case and it passes immediately, before you change any code. What should you do?</p>","options":[{"text":"Commit it; a passing test is the goal.","why":"A test you’ve never seen fail might not test what you think, for example because it calls the wrong function or asserts on the wrong value."},{"text":"Temporarily break the code (or the expectation) to confirm the test can fail, then decide whether the case was already covered.","why":"Correct. Either the behaviour already exists (fine, keep the test as documentation) or the test is broken; you need to know which."},{"text":"Delete the test, since TDD tests must start red.","why":"The test might be valuable documentation of a case; the point is to verify it can fail, not to throw it away."}],"answer":1},{"q":"<p>A user reports that settling up a group with one person owing $0.01 crashes. What’s the TDD way to fix it?</p>","options":[{"text":"Fix the code, then add a test for the case so it stays fixed.","why":"Better than no test, but you never saw the test fail, so you can’t be sure it reproduces the reported bug."},{"text":"Add a <code dir=\"ltr\">try/catch</code> around the settle-up call so the crash can’t reach the user.","why":"That hides the bug instead of fixing it, and the balances are still wrong."},{"text":"Write a test that reproduces the crash and watch it fail, then fix the code until it passes.","why":"Correct. The red test proves you’ve reproduced the bug; the green one proves the fix; the test then guards against regressions forever."}],"answer":2},{"q":"<p>Which task is the weakest fit for strict test-first development?</p>","options":[{"text":"Implementing the rounding rules for splitting a bill.","why":"Clear inputs and outputs with tricky edge cases are where TDD is strongest."},{"text":"Fixing a reported bug in the balance calculation.","why":"Reproducing a bug with a failing test first is one of TDD’s most valuable uses."},{"text":"Trying three layouts for the balances screen to see which one users understand.","why":"Correct. When you don’t know what you’re building yet, tests written first describe guesses; spike first, then test what you keep."}],"answer":2}],"exercise":{"kind":"web","mode":"js","starter":{"js":"// Make the tests (the checks) pass, one at a time.\n// balances: { name: cents }. Positive = is owed money, negative = owes.\n// Return transfers: [{ from, to, amountCents }].\nfunction settleUp(balances) {\n  return [];\n}\n\nconsole.log(settleUp({ Ana: 500, Ben: -500 }));\n"},"solution":{"js":"// Make the tests (the checks) pass, one at a time.\n// balances: { name: cents }. Positive = is owed money, negative = owes.\n// Return transfers: [{ from, to, amountCents }].\nfunction settleUp(balances) {\n  const entries = Object.entries(balances);\n  const sum = entries.reduce((total, [, cents]) => total + cents, 0);\n  if (sum !== 0) throw new Error('Balances must add up to zero');\n\n  const debtors = [];\n  const creditors = [];\n  for (const [name, cents] of entries) {\n    if (cents < 0) debtors.push({ name, cents: -cents });\n    else if (cents > 0) creditors.push({ name, cents });\n  }\n  const largestFirst = (a, b) => b.cents - a.cents || a.name.localeCompare(b.name);\n  debtors.sort(largestFirst);\n  creditors.sort(largestFirst);\n\n  const transfers = [];\n  let d = 0;\n  let c = 0;\n  while (d < debtors.length && c < creditors.length) {\n    const amountCents = Math.min(debtors[d].cents, creditors[c].cents);\n    transfers.push({ from: debtors[d].name, to: creditors[c].name, amountCents });\n    debtors[d].cents -= amountCents;\n    creditors[c].cents -= amountCents;\n    if (debtors[d].cents === 0) d += 1;\n    if (creditors[c].cents === 0) c += 1;\n  }\n  return transfers;\n}\n\nconsole.log(settleUp({ Ana: 500, Ben: -500 }));\nconsole.log(settleUp({ Ana: 1500, Ben: -1000, Cai: -500 }));\n"},"checks":[{"id":"settled","test":"const t = settleUp({ Ana: 0, Ben: 0 }); return Array.isArray(t) && t.length === 0 && Array.isArray(settleUp({})) && settleUp({}).length === 0;"},{"id":"two-people","test":"const t = settleUp({ Ana: 500, Ben: -500 }); return Array.isArray(t) && t.length === 1 && t[0].from === 'Ben' && t[0].to === 'Ana' && t[0].amountCents === 500;"},{"id":"settles-everyone","test":"const cases = [\n  { Ana: 500, Ben: -500 },\n  { Ana: 1500, Ben: -1000, Cai: -500 },\n  { Ana: -700, Ben: 300, Cai: 400, Dee: 0 },\n  { Ana: 2500, Ben: -1000, Cai: -1000, Dee: -1000, Eve: 500 },\n  { Ana: -1, Ben: -1, Cai: 2 },\n];\nfor (const b of cases) {\n  const left = { ...b };\n  const t = settleUp({ ...b });\n  if (!Array.isArray(t) || (t.length === 0)) return false;\n  for (const x of t) {\n    if (!Number.isInteger(x.amountCents) || x.amountCents <= 0) return false;\n    if (!(x.from in left) || !(x.to in left)) return false;\n    left[x.from] += x.amountCents;\n    left[x.to] -= x.amountCents;\n  }\n  if (Object.values(left).some((v) => v !== 0)) return false;\n}\nreturn true;"},{"id":"few-transfers","test":"const cases = [\n  { Ana: 500, Ben: -500 },\n  { Ana: 1500, Ben: -1000, Cai: -500 },\n  { Ana: -700, Ben: 300, Cai: 400, Dee: 0 },\n  { Ana: 2500, Ben: -1000, Cai: -1000, Dee: -1000, Eve: 500 },\n  { Ana: -1, Ben: -1, Cai: 2 },\n];\nreturn cases.every((b) => {\n  const people = Object.values(b).filter((v) => v !== 0).length;\n  const t = settleUp({ ...b });\n  return Array.isArray(t) && t.length >= 1 && t.length <= people - 1;\n});"},{"id":"ignores-settled","test":"const t = settleUp({ Ana: -700, Ben: 300, Cai: 400, Dee: 0 }); return Array.isArray(t) && t.length > 0 && t.every((x) => x.from !== 'Dee' && x.to !== 'Dee');"},{"id":"rejects-unbalanced","test":"try { settleUp({ Ana: 500, Ben: -400 }); return false; } catch (e) { return e instanceof Error; }"},{"id":"no-mutation","test":"const b = { Ana: 1500, Ben: -1000, Cai: -500 }; settleUp(b); return b.Ana === 1500 && b.Ben === -1000 && b.Cai === -500 && Object.keys(b).length === 3;"}],"title":"Make the settleUp tests pass","prompt":"<p>This is the <strong>green</strong> step of TDD: the tests are already written (they’re the checks below), and your job is to make them pass.</p>\n<p><code dir=\"ltr\">settleUp(balances)</code> takes balances in cents (<code dir=\"ltr\">{ Ana: 1500, Ben: -1000, Cai: -500 }</code>: positive means the person is owed money, negative means they owe) and returns transfers such as <code dir=\"ltr\">[{ from: 'Ben', to: 'Ana', amountCents: 1000 }, { from: 'Cai', to: 'Ana', amountCents: 500 }]</code>.</p>\n<p>The tests require that: settled groups need no transfers; one debtor pays one creditor; applying the transfers brings <strong>everyone to zero</strong> with positive whole-cent amounts; there are <strong>at most people − 1</strong> transfers (counting people with a non-zero balance); settled people never appear; balances that don’t add up to zero <strong>throw</strong>; and the input object is <strong>not modified</strong>.</p>\n<p>Work through the checks in order, running after each change. The starter passes the first one.</p>\n","hints":["First reject bad input: add up all the balances and throw an <code dir=\"ltr\">Error</code> if the sum is not 0.","Split people into debtors (negative balances, stored as positive amounts they owe) and creditors (positive balances). Copy the numbers into new objects so you never change the input.","Loop while both lists have someone: transfer <code dir=\"ltr\">Math.min(debtor.cents, creditor.cents)</code> from the current debtor to the current creditor, subtract it from both, and move to the next debtor or creditor when theirs reaches 0. Sorting both lists largest-first keeps the number of transfers small."],"explanation":"<p>The reference solution checks the sum first, then builds fresh <code dir=\"ltr\">{ name, cents }</code> objects for debtors and creditors, which is why the input stays untouched. The loop always transfers the smaller of the two current amounts, so after each transfer at least one person reaches zero and is left behind; that guarantees termination and at most <code dir=\"ltr\">people - 1</code> transfers. Sorting largest-first is a common heuristic that often produces fewer transfers in practice, but the tests deliberately don’t demand an exact list for groups larger than two, because several answers are correct. That’s a design choice worth copying: when many outputs are valid, assert the <strong>properties</strong> every valid output has (everyone ends at zero, amounts are positive, the count is small) instead of one specific answer.</p>\n","checkLabels":{"settled":"Settled groups (and an empty one) need no transfers","two-people":"With two people, the debtor pays the creditor the full amount","settles-everyone":"Applying the transfers brings everyone to zero, with positive whole-cent amounts","few-transfers":"Uses at most people − 1 transfers","ignores-settled":"People with a zero balance never appear in a transfer","rejects-unbalanced":"Throws when the balances don’t add up to zero","no-mutation":"Does not modify the balances object it was given"}},"words":706,"readMinutes":3}