{"id":"l-tj-1-1","courseId":"testing-javascript","lang":"en","kind":"intro","summary":"Explain what automated tests buy a team, name the four layers of the testing trophy, and pick a sensible layer for a given risk in a small app.","html":"<p>Last spring a teammate of mine changed one line in a bill-splitting helper: <code dir=\"ltr\">Math.round</code> became <code dir=\"ltr\">Math.floor</code>, to “fix” a share that looked one cent too high. Every screen still worked. Three weeks later a user noticed their group’s balances no longer added up to zero, and nobody could say which of 200 commits had done it. One three-line test would have failed within a second of that change.</p>\n<p>That is what tests buy you: <strong>the confidence to change code quickly</strong>. Without them, every refactor is a gamble and every release needs someone clicking through the app. With good ones, you change the code, run the suite, and know.</p>\n<h2 id=\"sec-1\" tabindex=\"-1\">What a test is</h2>\n<p>Strip away the frameworks and a test is code that runs your code and <strong>throws when the result is wrong</strong>:</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\">  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:#CF222E;--shiki-dark:#FF7B72\">const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> shares</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\">3</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\"> sum</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> shares.</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 style=\"color:#CF222E;--shiki-dark:#FF7B72\">if</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (sum </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">!==</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 1000</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) </span><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\"> Error</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">`Expected shares to add up to 1000, got ${</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">sum</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">}`</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:#0A3069;--shiki-dark:#A5D6FF\">'PASS shares add up to the total'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, shares);</span></span></code></pre><div class=\"run-output\" data-run-output=\"0\" hidden></div></div>\n<p>Vitest, Jest and Playwright add a lot on top: nice failure messages, watch mode, parallel runs, browsers. Underneath, <code dir=\"ltr\">expect(sum).toBe(1000)</code> is an <code dir=\"ltr\">if</code> that throws.</p>\n<h2 id=\"sec-2\" tabindex=\"-1\">Tests cost something too</h2>\n<p>Every test is code you maintain. It takes time to write, CI minutes to run, and attention when it fails. A test that fails when nothing is broken (a <em>flaky</em> or <em>brittle</em> test) is worse than no test: people learn to ignore red builds. So the question is never “how many tests?” but <strong>how much confidence per minute of CI</strong> each one buys.</p>\n<h2 id=\"sec-3\" tabindex=\"-1\">The testing trophy</h2>\n<p>The <em>testing trophy</em> is a popular way to picture the layers and how much of each to write:</p>\n<figure class=\"diagram\"><svg class=\"diagram-svg\" focusable=\"false\" viewBox=\"0 0 640 330\" role=\"img\" aria-labelledby=\"l-tj-1-1-t1\"><defs><marker id=\"arrow-l-tj-1-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-1-1-t1\">A trophy shape with four layers: static analysis at the base, a thin unit layer, a wide integration layer, and a small end-to-end layer on top.</title>\n  <rect class=\"d-box-success\" x=\"250\" y=\"20\" width=\"140\" height=\"48\" rx=\"10\"/>\n  <text class=\"d-label-strong\" x=\"320\" y=\"49\" text-anchor=\"middle\">End-to-end</text>\n  <rect class=\"d-box-primary\" x=\"170\" y=\"80\" width=\"300\" height=\"96\" rx=\"14\"/>\n  <text class=\"d-label-strong\" x=\"320\" y=\"122\" text-anchor=\"middle\">Integration</text>\n  <text class=\"d-label-muted\" x=\"320\" y=\"148\" text-anchor=\"middle\">components + modules together</text>\n  <rect class=\"d-box-accent\" x=\"220\" y=\"188\" width=\"200\" height=\"52\" rx=\"10\"/>\n  <text class=\"d-label-strong\" x=\"320\" y=\"219\" text-anchor=\"middle\">Unit</text>\n  <rect class=\"d-box-warn\" x=\"190\" y=\"252\" width=\"260\" height=\"52\" rx=\"10\"/>\n  <text class=\"d-label-strong\" x=\"320\" y=\"283\" text-anchor=\"middle\">Static: types + lint</text>\n  <path class=\"d-arrow\" d=\"M520 290 L520 40\" marker-end=\"url(#arrow-l-tj-1-1-1)\"/>\n  <text class=\"d-label-muted\" x=\"532\" y=\"168\">slower,</text>\n  <text class=\"d-label-muted\" x=\"532\" y=\"188\">costlier,</text>\n  <text class=\"d-label-muted\" x=\"532\" y=\"208\">more real</text>\n</svg><figcaption>The testing trophy: integration tests give the most confidence for their cost</figcaption></figure>\n<ul>\n<li><strong>Static analysis</strong> (TypeScript, ESLint) catches typos and wrong types before anything runs.</li>\n<li><strong>Unit tests</strong> check one function or module in isolation. Fast and precise; ideal for pure logic like money rounding.</li>\n<li><strong>Integration tests</strong> check several units working together, such as a form component calling the real parsing code. They catch most real bugs for a modest cost, which is why the trophy is widest here.</li>\n<li><strong>End-to-end tests</strong> drive the real app in a real browser. The most realistic and the most expensive; keep them for the flows that would make the news if they broke.</li>\n</ul>\n<p>The rule I use daily: <strong>pick the cheapest test that would fail if the thing you are worried about broke.</strong></p>\n<h2 id=\"sec-4\" tabindex=\"-1\">What you build in this course</h2>\n<p>The running project is <strong>Splitwise-lite</strong>, a bill-splitting module for a group trip: parse amounts, split bills, add tips, compute who owes whom and settle up, plus a small UI and a network layer. In the playground you get working code and write tests against it. Then the checker runs your tests against deliberately broken copies (a wrong rounding, an off-by-one, swapped arguments) and your tests must catch each one. Passing on good code is half the job; failing on bad code is the other half.</p>\n<p>Before installing anything, build the core idea yourself in the exercise below: an assertion that throws and a runner that catches.</p>\n","toc":[{"id":"sec-1","text":"What a test is","level":2},{"id":"sec-2","text":"Tests cost something too","level":2},{"id":"sec-3","text":"The testing trophy","level":2},{"id":"sec-4","text":"What you build in this course","level":2}],"takeaways":["A test is code that runs your code and throws when the result is wrong; every framework is a nicer wrapper around that idea.","Tests buy you the confidence to change code quickly; a test that never catches a bug and never documents anything costs more than it earns.","The testing trophy has four layers (static analysis, unit, integration, end-to-end), with integration tests usually giving the most confidence per minute of CI.","Choose the cheapest test that would fail if the thing you are worried about broke."],"further":[{"title":"Vitest — Why Vitest","url":"https://vitest.dev/guide/why"},{"title":"Playwright — Getting started","url":"https://playwright.dev/docs/intro"},{"title":"Testing Library — Guiding principles","url":"https://testing-library.com/docs/guiding-principles"}],"quiz":[{"q":"<p>Splitwise-lite rounds each person’s share to whole cents. Which test gives you the fastest, most precise signal that the rounding is right?</p>","options":[{"text":"An end-to-end test that adds an expense in the browser and reads the balance.","why":"It would catch the bug, but it is slow, needs a running app and points at the whole stack when it fails, not at the rounding function."},{"text":"A unit test that calls the split function with 1000 cents and 3 people.","why":"Correct. Rounding is pure logic with many edge cases, so a millisecond unit test is the cheapest test that fails exactly when the rounding breaks."},{"text":"A TypeScript type check on the function signature.","why":"Types confirm the function takes and returns numbers, but they cannot tell 334 from 333; rounding is a value question, not a type question."}],"answer":1},{"q":"<p>What does a test framework fundamentally do when an assertion fails?</p>","options":[{"text":"It edits the source code so the assertion passes next time.","why":"No framework changes your code; a failing test is information for you to act on."},{"text":"It logs a warning and keeps the test green.","why":"A test that stays green on a failed assertion would be useless; frameworks mark the test as failed."},{"text":"The assertion throws, the runner catches the error, marks that test failed and moves on.","why":"Correct. <code dir=\"ltr\">expect(...).toBe(...)</code> throws an error on mismatch; the runner wraps each test in a try/catch and reports it."}],"answer":2},{"q":"<p>Your team’s end-to-end suite takes 40 minutes and fails randomly about once a day. Which change most improves confidence per minute of CI?</p>","options":[{"text":"Move most of the logic checks down to integration and unit tests, keeping a few end-to-end tests for critical flows.","why":"Correct. Lower layers check the same logic in milliseconds without browser flakiness; a handful of end-to-end tests still prove the pieces connect."},{"text":"Add automatic retries until every test passes.","why":"Retries hide flakiness instead of fixing it and make the suite even slower; real bugs that fail intermittently get retried away too."},{"text":"Delete the end-to-end suite and rely on unit tests only.","why":"Unit tests can all pass while the app is broken, for example when the form never calls the function they test. You still want a few end-to-end tests."}],"answer":0}],"exercise":{"kind":"web","mode":"js","starter":{"js":"// An assertion: throw an Error when actual !== expected.\nfunction expectEqual(actual, expected) {\n  // your code\n}\n\n// A runner: run fn, log PASS or FAIL, never let one failure stop the others.\nfunction test(name, fn) {\n  // your code\n}\n\ntest(\"two plus two\", () => expectEqual(2 + 2, 4));\ntest(\"deliberately wrong\", () => expectEqual(0.1 + 0.2, 0.3));\n"},"solution":{"js":"function expectEqual(actual, expected) {\n  if (actual !== expected) {\n    throw new Error(`Expected ${expected} but received ${actual}`);\n  }\n}\n\nfunction test(name, fn) {\n  try {\n    fn();\n    console.log(`PASS ${name}`);\n  } catch (error) {\n    console.log(`FAIL ${name}: ${error.message}`);\n  }\n}\n\ntest(\"two plus two\", () => expectEqual(2 + 2, 4));\ntest(\"deliberately wrong\", () => expectEqual(0.1 + 0.2, 0.3));\n"},"checks":[{"id":"equal-passes","test":"try { expectEqual(5, 5); expectEqual(\"a\", \"a\"); return true; } catch { return false; }"},{"id":"unequal-throws","test":"try { expectEqual(333, 334); return false; } catch (e) { return e instanceof Error && String(e.message).includes(\"333\") && String(e.message).includes(\"334\"); }"},{"id":"logs-pass","test":"test(\"check-pass-case\", () => expectEqual(1, 1)); return logs.includes(\"PASS check-pass-case\");"},{"id":"logs-fail","test":"test(\"check-fail-case\", () => expectEqual(1, 2)); return logs.some((l) => l.startsWith(\"FAIL check-fail-case\") && l.includes(\"2\"));"},{"id":"keeps-going","test":"try { test(\"check-boom\", () => { throw new Error(\"boom\"); }); } catch { return false; } return logs.some((l) => l.startsWith(\"FAIL check-boom\"));"}],"title":"Build a ten-line test runner","prompt":"<p>Write the two functions every test framework is built on.</p>\n<ul>\n<li><code dir=\"ltr\">expectEqual(actual, expected)</code> does nothing when the values are strictly equal (<code dir=\"ltr\">===</code>) and <strong>throws an <code dir=\"ltr\">Error</code></strong> otherwise. The message must include both values, for example <code dir=\"ltr\">Expected 334 but received 333</code>.</li>\n<li><code dir=\"ltr\">test(name, fn)</code> calls <code dir=\"ltr\">fn</code>. If it returns normally, log <code dir=\"ltr\">PASS &lt;name&gt;</code>. If it throws, log <code dir=\"ltr\">FAIL &lt;name&gt;: &lt;error message&gt;</code> and <strong>do not rethrow</strong>, so the next test still runs.</li>\n</ul>\n<p>Run it: the second test fails on purpose, because <code dir=\"ltr\">0.1 + 0.2</code> is not exactly <code dir=\"ltr\">0.3</code> in JavaScript.</p>\n","hints":["<code dir=\"ltr\">expectEqual</code> needs one <code dir=\"ltr\">if</code> and one <code dir=\"ltr\">throw new Error(...)</code>. Use a template literal to put both values in the message.","<code dir=\"ltr\">test</code> needs a <code dir=\"ltr\">try { … } catch (error) { … }</code> around the call to <code dir=\"ltr\">fn()</code>.","Log with <code dir=\"ltr\">console.log(`PASS ${name}`)</code> after <code dir=\"ltr\">fn()</code> inside <code dir=\"ltr\">try</code>, and <code dir=\"ltr\">console.log(`FAIL ${name}: ${error.message}`)</code> inside <code dir=\"ltr\">catch</code>."],"explanation":"<p>This is the core of Vitest, Jest and every other runner: an assertion is an <code dir=\"ltr\">if</code> that throws, and a test is a function the runner calls inside <code dir=\"ltr\">try/catch</code>. Catching the error is what lets one failing test be reported without stopping the rest of the suite. Real frameworks add better messages (diffs of objects), async support, hooks, watch mode and parallel runs, but the contract is identical: <strong>a test passes when its function finishes without throwing</strong>. Notice also the deliberate failure: <code dir=\"ltr\">0.1 + 0.2</code> is <code dir=\"ltr\">0.30000000000000004</code>, which is exactly why Splitwise-lite stores money as integer cents.</p>\n","checkLabels":{"equal-passes":"expectEqual does not throw when the values are equal","unequal-throws":"expectEqual throws an Error that names both values","logs-pass":"test logs PASS <name> for a passing test","logs-fail":"test logs FAIL <name>: <message> for a failing test","keeps-going":"test catches the error instead of rethrowing it"}},"words":486,"readMinutes":3}