{"id":"l-tj-1-3","courseId":"testing-javascript","lang":"en","kind":"lesson","summary":"Structure every test as Arrange, Act, Assert, choose between toBe and toEqual, and read a Vitest failure to find the broken line in seconds.","html":"<p>A test is read far more often than it is written. It is read when it fails at a bad moment, by someone who didn’t write it, who wants one answer: <em>what broke?</em> This lesson is about writing tests that answer that in seconds, and about reading the answer Vitest gives you.</p>\n<h2 id=\"sec-1\" tabindex=\"-1\">Arrange, Act, Assert</h2>\n<p>Nearly every good test has the same three parts:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"js\" data-index=\"0\"><div class=\"code-head\"><span class=\"code-title\">src/tip.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\"> { 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\"> { withTip } </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">from</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> './tip.js'</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\">test</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'rounds a half-cent tip up to the next 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:#6E7781;--shiki-dark:#8B949E\">  // Arrange: the inputs that matter for this behaviour</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> subtotalCents</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 1980</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\"> percent</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 12.5</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\">  // Act: one call to the code under test</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> result</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> withTip</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(subtotalCents, percent);</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#6E7781;--shiki-dark:#8B949E\">  // Assert: the claims about the outcome</span></span>\n<span class=\"line\"><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">  expect</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(result).</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">toEqual</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">({ tipCents: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">248</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, totalCents: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">2228</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><strong>Arrange</strong> builds the world the test needs. <strong>Act</strong> does exactly one thing: the behaviour you are testing. <strong>Assert</strong> states what should now be true. You don’t need the comments once the shape is a habit; the blank lines are enough.</p>\n<p>The shape protects you from the most common smell: a test that acts, asserts, acts again, asserts again. When that fails, you can’t tell which step broke. Split it into two tests, each with a name that says what it proves.</p>\n<figure class=\"diagram\"><svg class=\"diagram-svg\" focusable=\"false\" viewBox=\"0 0 640 170\" role=\"img\" aria-labelledby=\"l-tj-1-3-t1\"><defs><marker id=\"arrow-l-tj-1-3-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-3-t1\">Three boxes in a row: Arrange builds inputs, Act calls withTip once, Assert compares the result with toEqual.</title>\n  <rect class=\"d-box-accent\" x=\"20\" y=\"40\" width=\"170\" height=\"90\" rx=\"12\"/>\n  <text class=\"d-label-strong\" x=\"105\" y=\"76\" text-anchor=\"middle\">Arrange</text>\n  <text class=\"d-code\" x=\"105\" y=\"104\" text-anchor=\"middle\">1980, 12.5</text>\n  <path class=\"d-arrow\" d=\"M190 85 L232 85\" marker-end=\"url(#arrow-l-tj-1-3-1)\"/>\n  <rect class=\"d-box-primary\" x=\"235\" y=\"40\" width=\"170\" height=\"90\" rx=\"12\"/>\n  <text class=\"d-label-strong\" x=\"320\" y=\"76\" text-anchor=\"middle\">Act</text>\n  <text class=\"d-code\" x=\"320\" y=\"104\" text-anchor=\"middle\">withTip(...)</text>\n  <path class=\"d-arrow\" d=\"M405 85 L447 85\" marker-end=\"url(#arrow-l-tj-1-3-1)\"/>\n  <rect class=\"d-box-success\" x=\"450\" y=\"40\" width=\"170\" height=\"90\" rx=\"12\"/>\n  <text class=\"d-label-strong\" x=\"535\" y=\"76\" text-anchor=\"middle\">Assert</text>\n  <text class=\"d-code\" x=\"535\" y=\"104\" text-anchor=\"middle\">toEqual({...})</text>\n</svg><figcaption>Arrange, Act, Assert: one setup, one action, the claims</figcaption></figure>\n<h3 id=\"sec-2\" tabindex=\"-1\">How many assertions per test?</h3>\n<p>“One assertion per test” is a rule you will hear, and it is too strict. The real rule is <strong>one behaviour per test</strong>. Checking both <code dir=\"ltr\">tipCents</code> and <code dir=\"ltr\">totalCents</code> after a single call is one behaviour with two observable facts, so asserting both is fine, and <code dir=\"ltr\">toEqual</code> on the whole object does it in one line. What you want to avoid is a test that checks rounding, then calls the function again to check the error case, then a third time for zero. When the first claim fails, Vitest stops that test, so you never learn whether the other two behaviours still work. Three small tests report three separate answers.</p>\n<h2 id=\"sec-3\" tabindex=\"-1\">Names are the first line of the bug report</h2>\n<p>When a test fails, the report prints its full path: <code dir=\"ltr\">withTip &gt; rounds a half-cent tip up to the next cent</code>. Write that name as a sentence about behaviour. “withTip works” tells you nothing; “rejects a negative tip percent” tells you what regressed before you open a file. Group with <code dir=\"ltr\">describe</code> by unit, name with <code dir=\"ltr\">test</code> by rule.</p>\n<h2 id=\"sec-4\" tabindex=\"-1\">toBe or toEqual?</h2>\n<p>Two matchers do most of the work, and they compare differently:</p>\n<ul>\n<li><code dir=\"ltr\">toBe</code> uses <code dir=\"ltr\">Object.is</code>: same primitive value, or the <strong>same object reference</strong>. Use it for numbers, strings and booleans.</li>\n<li><code dir=\"ltr\">toEqual</code> compares <strong>structure</strong>, recursively: same keys, same values. Use it for objects and arrays. (<code dir=\"ltr\">toStrictEqual</code> also checks that <code dir=\"ltr\">undefined</code> properties and class types match.)</li>\n</ul>\n<p><code dir=\"ltr\">expect(withTip(2000, 15)).toBe({ tipCents: 300, totalCents: 2300 })</code> fails even though the values match, because the function returns a new object. This trips up everyone once.</p>\n<h2 id=\"sec-5\" tabindex=\"-1\">Reading a failure</h2>\n<p>Here is a real kind of bug. A first version of <code dir=\"ltr\">toCents</code>, which turns a dollar amount into cents, used <code dir=\"ltr\">Math.floor</code>:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"js\" data-run=\"js\" data-index=\"1\"><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=\"1\" 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\">const</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> toCents</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (</span><span style=\"color:#953800;--shiki-dark:#FFA657\">dollars</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) </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\">(dollars </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">*</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 100</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:#0550AE;--shiki-dark:#79C0FF\">0.29</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> *</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 100</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);    </span><span style=\"color:#6E7781;--shiki-dark:#8B949E\">// what the computer actually computes</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\">toCents</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">0.29</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">)); </span><span style=\"color:#6E7781;--shiki-dark:#8B949E\">// one cent short</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\">(Math.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">round</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">0.29</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> *</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 100</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">));</span></span></code></pre><div class=\"run-output\" data-run-output=\"1\" hidden></div></div>\n<p>The test <code dir=\"ltr\">expect(toCents(0.29)).toBe(29)</code> fails, and Vitest reports something like this:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"text\" data-index=\"2\"><div class=\"code-head\"><span class=\"code-title\">Text</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> FAIL  src/money.test.js &gt; toCents &gt; converts 0.29 dollars to 29 cents\nAssertionError: expected 28 to be 29 // Object.is equality\n\n- Expected\n+ Received\n\n- 29\n+ 28\n\n ❯ src/money.test.js:7:28</code></pre></div>\n<p>Read it top to bottom, in three passes:</p>\n<ol>\n<li><strong>The path</strong> says which behaviour broke: converting 0.29 dollars.</li>\n<li><strong>Expected versus received</strong> says how: one cent short, so something truncated instead of rounding.</li>\n<li><strong>The location</strong> (<code dir=\"ltr\">money.test.js:7:28</code>) points at the assertion line in the test. The bug is in the code that line calls.</li>\n</ol>\n<p>For objects, the diff marks only the lines that differ, so a wrong <code dir=\"ltr\">totalCents</code> inside a ten-key object stands out immediately.</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>Weakening the assertion to make it pass</span></p>\n<p>Faced with <code dir=\"ltr\">expected 28 to be 29</code>, it is tempting to change the expectation to 28 (“that’s what the code returns”) or to round the value inside the test, and move on. The test was right; the code was wrong. Change an assertion only when you can explain why the old expectation was a mistake. This is also why Splitwise-lite stores <strong>integer cents</strong>: integers add up exactly, so <code dir=\"ltr\">toBe</code> is always the right matcher for money.</p>\n</aside>\n<h2 id=\"sec-6\" tabindex=\"-1\">Testing that something throws</h2>\n<p>Sometimes the behaviour is “refuse bad input”. Wrap the call in a function so <code dir=\"ltr\">expect</code> can call it and catch the error:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"js\" data-index=\"3\"><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\">'rejects a negative tip percent'</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\"> withTip</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:#CF222E;--shiki-dark:#FF7B72\">-</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">5</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></code></pre></div>\n<p>Without the arrow function, <code dir=\"ltr\">withTip</code> throws while the arguments are being evaluated, before <code dir=\"ltr\">expect</code> runs, and the test crashes instead of passing. <code dir=\"ltr\">toThrow</code> also accepts a string or regex to match the message.</p>\n<p>In the exercise you write AAA tests for <code dir=\"ltr\">withTip</code>, including the boundary that is easy to forget: a tip of zero. The next section moves from “how a test looks” to “which tests are worth writing”.</p>\n","toc":[{"id":"sec-1","text":"Arrange, Act, Assert","level":2},{"id":"sec-2","text":"How many assertions per test?","level":3},{"id":"sec-3","text":"Names are the first line of the bug report","level":2},{"id":"sec-4","text":"toBe or toEqual?","level":2},{"id":"sec-5","text":"Reading a failure","level":2},{"id":"sec-6","text":"Testing that something throws","level":2}],"takeaways":["Arrange, Act, Assert gives every test the same three-part shape, so readers find the setup, the action and the claim without hunting.","One test checks one behaviour, and its name is a sentence that says which behaviour broke when it fails.","<code dir=\"ltr\">toBe</code> compares with <code dir=\"ltr\">Object.is</code> (same value or same reference); <code dir=\"ltr\">toEqual</code> compares structure, which is what you want for objects and arrays.","A failure report gives you the test path, the expected and received values, and the file and line; read all three before touching code."],"further":[{"title":"Vitest — expect API","url":"https://vitest.dev/api/expect"},{"title":"Vitest — Test API","url":"https://vitest.dev/api/test"},{"title":"MDN — Number.EPSILON and floating point","url":"https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/EPSILON"}],"quiz":[{"q":"<p>What does this test report?</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"js\" data-index=\"0\"><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\">'builds the tip summary'</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\">withTip</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">2000</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">15</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\">({ tipCents: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">300</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, totalCents: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">2300</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","options":[{"text":"It passes, because the object has the right values.","why":"The values are right, but <code dir=\"ltr\">toBe</code> checks identity with <code dir=\"ltr\">Object.is</code>, and two separately created objects are never the same reference."},{"text":"It fails, because <code dir=\"ltr\">toBe</code> compares references and the function returns a new object.","why":"Correct. Use <code dir=\"ltr\">toEqual</code> (or <code dir=\"ltr\">toStrictEqual</code>) to compare objects by their contents."},{"text":"It throws a syntax error because objects can’t be passed to <code dir=\"ltr\">toBe</code>.","why":"Any value can be passed to <code dir=\"ltr\">toBe</code>; the problem is the comparison it performs, not the syntax."}],"answer":1},{"q":"<p>A failing test prints <code dir=\"ltr\">expected 28 to be 29</code> for <code dir=\"ltr\">toCents(0.29)</code>. Where do you look first?</p>","options":[{"text":"At the test, to change the expected value to 28 so it matches what the code returns.","why":"That makes the test agree with the bug; cents are integers and 28 is a whole cent wrong."},{"text":"At the test runner config, because floating point is handled differently in Vitest.","why":"Vitest runs plain JavaScript; <code dir=\"ltr\">0.29 * 100</code> is <code dir=\"ltr\">28.999999999999996</code> everywhere, so the config is not involved."},{"text":"At how <code dir=\"ltr\">toCents</code> turns the float into an integer, because <code dir=\"ltr\">0.29 * 100</code> is slightly below 29.","why":"Correct. The received value tells you the code truncated a value that was a hair under 29; <code dir=\"ltr\">Math.round</code> fixes it, <code dir=\"ltr\">Math.floor</code> causes it."}],"answer":2},{"q":"<p>Which test name is most useful when it fails in CI at 2 a.m.?</p>","options":[{"text":"<code dir=\"ltr\">test('withTip works')</code>","why":"It says which function, not which behaviour; you still have to open the file to learn what broke."},{"text":"<code dir=\"ltr\">test('test 3')</code>","why":"A number carries no information and becomes wrong as soon as tests are reordered."},{"text":"<code dir=\"ltr\">test('rounds a half-cent tip up to the next cent')</code>","why":"Correct. The name states the rule, so the failure report alone tells you which behaviour regressed."},{"text":"<code dir=\"ltr\">test('withTip(1980, 12.5)')</code>","why":"The input is visible but not the expected behaviour; the reader can’t tell what the right output was meant to be."}],"answer":2},{"q":"<p>Why do Arrange, Act and Assert usually appear in that order, separated by a blank line?</p>","options":[{"text":"Vitest requires the three phases in that order or it skips the test.","why":"Vitest has no idea about phases; AAA is a convention for humans, not a rule the runner enforces."},{"text":"It makes setup, the single action under test and the claims easy to find at a glance.","why":"Correct. A consistent shape means a reader can jump straight to the action and the assertion in any test."},{"text":"It lets Vitest run the Arrange phase once for the whole file.","why":"Shared setup belongs in hooks like <code dir=\"ltr\">beforeEach</code>; the blank lines in a test change nothing at runtime."}],"answer":1}],"exercise":{"kind":"web","mode":"test","subject":"// Adds a tip to a subtotal. Money is in integer cents; percent is a\n// number such as 15 or 12.5. The tip is rounded to the nearest cent\n// (a half cent rounds up). A zero tip is allowed; a negative one is not.\nfunction withTip(subtotalCents, percent) {\n  if (percent < 0) throw new RangeError('Tip percent cannot be negative');\n  const tipCents = Math.round((subtotalCents * percent) / 100);\n  return { tipCents, totalCents: subtotalCents + tipCents };\n}\n","mutants":[{"id":"floors-tip","code":"// Adds a tip to a subtotal. Money is in integer cents; percent is a\n// number such as 15 or 12.5. The tip is rounded to the nearest cent\n// (a half cent rounds up). A zero tip is allowed; a negative one is not.\nfunction withTip(subtotalCents, percent) {\n  if (percent < 0) throw new RangeError('Tip percent cannot be negative');\n  const tipCents = Math.floor((subtotalCents * percent) / 100);\n  return { tipCents, totalCents: subtotalCents + tipCents };\n}\n"},{"id":"allows-negative","code":"// Adds a tip to a subtotal. Money is in integer cents; percent is a\n// number such as 15 or 12.5. The tip is rounded to the nearest cent\n// (a half cent rounds up). A zero tip is allowed; a negative one is not.\nfunction withTip(subtotalCents, percent) {\n  \n  const tipCents = Math.round((subtotalCents * percent) / 100);\n  return { tipCents, totalCents: subtotalCents + tipCents };\n}\n"},{"id":"rejects-zero","code":"// Adds a tip to a subtotal. Money is in integer cents; percent is a\n// number such as 15 or 12.5. The tip is rounded to the nearest cent\n// (a half cent rounds up). A zero tip is allowed; a negative one is not.\nfunction withTip(subtotalCents, percent) {\n  if (percent <= 0) throw new RangeError('Tip percent cannot be negative');\n  const tipCents = Math.round((subtotalCents * percent) / 100);\n  return { tipCents, totalCents: subtotalCents + tipCents };\n}\n"}],"starter":{"js":"describe('withTip', () => {\n  test('adds a 15% tip to the subtotal', () => {\n    // Arrange\n    const subtotalCents = 2000;\n\n    // Act\n    const result = withTip(subtotalCents, 15);\n\n    // Assert\n    expect(result).toEqual({ tipCents: 300, totalCents: 2300 });\n  });\n\n  // Add: a half-cent tip that must round up,\n  // a zero tip, and a negative percent that must throw.\n});\n"},"solution":{"js":"describe('withTip', () => {\n  test('adds a 15% tip to the subtotal', () => {\n    const subtotalCents = 2000;\n\n    const result = withTip(subtotalCents, 15);\n\n    expect(result).toEqual({ tipCents: 300, totalCents: 2300 });\n  });\n\n  test('rounds a half-cent tip up to the next cent', () => {\n    const result = withTip(1980, 12.5);\n\n    expect(result).toEqual({ tipCents: 248, totalCents: 2228 });\n  });\n\n  test('allows a zero tip', () => {\n    const result = withTip(2000, 0);\n\n    expect(result).toEqual({ tipCents: 0, totalCents: 2000 });\n  });\n\n  test('rejects a negative tip percent', () => {\n    expect(() => withTip(2000, -5)).toThrow(RangeError);\n  });\n});\n"},"checks":[{"id":"passes","suite":"subject"},{"id":"catches-floors-tip","suite":"floors-tip"},{"id":"catches-allows-negative","suite":"allows-negative"},{"id":"catches-rejects-zero","suite":"rejects-zero"}],"title":"AAA tests for withTip","prompt":"<p><code dir=\"ltr\">withTip(subtotalCents, percent)</code> returns <code dir=\"ltr\">{ tipCents, totalCents }</code>. The tip is rounded to the nearest cent (a half cent rounds <strong>up</strong>), a 0% tip is valid, and a negative percent throws a <code dir=\"ltr\">RangeError</code>.</p>\n<p>The starter test checks a 15% tip on $20.00. Add tests in Arrange, Act, Assert shape until your suite passes on the real code and catches three hidden bugs: one floors the tip instead of rounding it, one forgets to reject negative percents, and one wrongly rejects a zero tip.</p>\n","hints":["A 15% tip on 2000 is exactly 300, so rounding never matters there. You need an input where the exact tip ends in .5 or more.","1980 cents at 12.5% is 247.5 cents, which must round to 248. Compare the whole object with <code dir=\"ltr\">toEqual</code>.","For the error, wrap the call: <code dir=\"ltr\">expect(() =&gt; withTip(2000, -5)).toThrow(RangeError)</code>. For zero, assert the result, not just that it doesn’t throw."],"explanation":"<p>Each added test pins one rule from the spec with an input that separates right from wrong. A tip that lands on a half cent tells <code dir=\"ltr\">Math.round</code> from <code dir=\"ltr\">Math.floor</code>. The zero tip catches the classic boundary slip (<code dir=\"ltr\">&lt;=</code> instead of <code dir=\"ltr\">&lt;</code>), which would make Splitwise-lite refuse a perfectly normal bill. The negative case needs the arrow function so <code dir=\"ltr\">expect</code> can catch the throw. Asserting the full <code dir=\"ltr\">{ tipCents, totalCents }</code> object with <code dir=\"ltr\">toEqual</code> also guards the total, so one assertion covers both fields.</p>\n","checkLabels":{"passes":"All your tests pass against the real withTip","catches-floors-tip":"Catches the version that floors the tip","catches-allows-negative":"Catches the version that accepts a negative percent","catches-rejects-zero":"Catches the version that rejects a zero tip"}},"words":709,"readMinutes":3}