{"id":"l-tj-2-4","courseId":"testing-javascript","lang":"en","kind":"lesson","summary":"Replace collaborators with vi.fn and vi.spyOn, control time with fake timers, and recognise when mocking makes a test pass while the real code is broken.","html":"<p>In the last lesson you wrote a stub API by hand, with a counter. It worked, and it was about fifteen lines. Vitest’s <code dir=\"ltr\">vi</code> helpers do the same in one line, and they add a lot of power. That power is the problem: mocking is the fastest way to write a test that passes while the real code is broken. This lesson covers the tools and the judgement.</p>\n<h2 id=\"sec-1\" tabindex=\"-1\">The vocabulary, briefly</h2>\n<p>All of these are <strong>test doubles</strong>, stand-ins for a real collaborator:</p>\n<figure class=\"diagram\"><svg class=\"diagram-svg\" focusable=\"false\" viewBox=\"0 0 680 190\" role=\"img\" aria-labelledby=\"l-tj-2-4-t1\"><defs><marker id=\"arrow-l-tj-2-4-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-4-t1\">Four kinds of test double in a row: a stub returns canned answers, a spy records calls, a mock is a spy you assert on, a fake is a working lightweight implementation.</title>\n  <rect class=\"d-box\" x=\"10\" y=\"40\" width=\"155\" height=\"110\" rx=\"12\"/>\n  <text class=\"d-label-strong\" x=\"87\" y=\"72\" text-anchor=\"middle\">Stub</text>\n  <text class=\"d-label-muted\" x=\"87\" y=\"100\" text-anchor=\"middle\">canned answers</text>\n  <text class=\"d-code\" x=\"87\" y=\"126\" text-anchor=\"middle\">returns {id}</text>\n  <rect class=\"d-box-accent\" x=\"180\" y=\"40\" width=\"155\" height=\"110\" rx=\"12\"/>\n  <text class=\"d-label-strong\" x=\"257\" y=\"72\" text-anchor=\"middle\">Spy</text>\n  <text class=\"d-label-muted\" x=\"257\" y=\"100\" text-anchor=\"middle\">records calls</text>\n  <text class=\"d-code\" x=\"257\" y=\"126\" text-anchor=\"middle\">.mock.calls</text>\n  <rect class=\"d-box-primary\" x=\"350\" y=\"40\" width=\"155\" height=\"110\" rx=\"12\"/>\n  <text class=\"d-label-strong\" x=\"427\" y=\"72\" text-anchor=\"middle\">Mock</text>\n  <text class=\"d-label-muted\" x=\"427\" y=\"100\" text-anchor=\"middle\">spy you assert on</text>\n  <text class=\"d-code\" x=\"427\" y=\"126\" text-anchor=\"middle\">CalledWith</text>\n  <rect class=\"d-box-success\" x=\"520\" y=\"40\" width=\"155\" height=\"110\" rx=\"12\"/>\n  <text class=\"d-label-strong\" x=\"597\" y=\"72\" text-anchor=\"middle\">Fake</text>\n  <text class=\"d-label-muted\" x=\"597\" y=\"100\" text-anchor=\"middle\">working, simpler</text>\n  <text class=\"d-code\" x=\"597\" y=\"126\" text-anchor=\"middle\">in-memory DB</text>\n</svg><figcaption>Test doubles, from simplest to most behaviour</figcaption></figure>\n<p>In Vitest, <code dir=\"ltr\">vi.fn()</code> gives you a stub and a spy in one object, and people call all of them “mocks”. The names matter less than the question each answers: <em>what does this collaborator return?</em> (stub) and <em>how was it called?</em> (spy).</p>\n<h2 id=\"sec-2\" tabindex=\"-1\">vi.fn: a function that remembers</h2>\n<p>Splitwise-lite sends a reminder to everyone who owes money. The sender is passed in, so a test can replace it:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"js\" data-index=\"0\"><div class=\"code-head\"><span class=\"code-title\">src/remind.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, vi } </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\"> { remindDebtors } </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">from</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> './remind.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\">'reminds people who owe money, with the amount'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, () </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> sendReminder</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\">fn</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:#0550AE;--shiki-dark:#79C0FF\"> sent</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> remindDebtors</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">({ Ana: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">1500</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\">, Cai: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">0</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> }, sendReminder);</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">  expect</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(sent).</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 style=\"color:#8250DF;--shiki-dark:#D2A8FF\">  expect</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(sendReminder).</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">toHaveBeenCalledTimes</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 style=\"color:#8250DF;--shiki-dark:#D2A8FF\">  expect</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(sendReminder).</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">toHaveBeenCalledWith</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'Ben'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'You owe $5.00 to the group'</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><code dir=\"ltr\">vi.fn()</code> returns <code dir=\"ltr\">undefined</code> by default. Give it behaviour when the code under test needs an answer:</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:#CF222E;--shiki-dark:#FF7B72\">const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> post</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\">fn</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">().</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">mockResolvedValue</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">({ id: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'e1'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> });          </span><span style=\"color:#6E7781;--shiki-dark:#8B949E\">// async success</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> failing</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\">fn</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">().</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">mockRejectedValue</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</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\">'Offline'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">)); </span><span style=\"color:#6E7781;--shiki-dark:#8B949E\">// async failure</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> send</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\">fn</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">().</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">mockImplementation</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">((</span><span style=\"color:#953800;--shiki-dark:#FFA657\">name</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\">  if</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (name </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">===</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'Ben'</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\">'Mailbox full'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">});</span></span></code></pre></div>\n<p>Every call is recorded in <code dir=\"ltr\">send.mock.calls</code>, an array of argument arrays, for the rare case the matchers don’t cover.</p>\n<h2 id=\"sec-3\" tabindex=\"-1\">vi.spyOn: watching a real method</h2>\n<p>Sometimes the collaborator is a method on an existing object, like <code dir=\"ltr\">console.warn</code>. <code dir=\"ltr\">vi.spyOn</code> wraps it: by default the real method still runs, and the spy records calls. Chain <code dir=\"ltr\">mockImplementation</code> to silence or replace it:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"js\" data-index=\"2\"><div class=\"code-head\"><span class=\"code-title\">JavaScript</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">import</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { afterEach, test, expect, vi } </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>\n<span class=\"line\"><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">afterEach</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:#1F2328;--shiki-dark:#E6EDF3\">  vi.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">restoreAllMocks</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(); </span><span style=\"color:#6E7781;--shiki-dark:#8B949E\">// put console.warn back</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\">'warns and carries on when one reminder fails'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, () </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> warn</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\">(console, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'warn'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">).</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">mockImplementation</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(() </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {});</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> send</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\">fn</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">().</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">mockImplementationOnce</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 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\">'Mailbox full'</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:#0550AE;--shiki-dark:#79C0FF\"> sent</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> remindDebtors</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\">, Dee: </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">-</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">300</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> }, send);</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">  expect</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(sent).</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 style=\"color:#8250DF;--shiki-dark:#D2A8FF\">  expect</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(send).</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">toHaveBeenCalledTimes</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>\n<span class=\"line\"><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">  expect</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(warn).</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">toHaveBeenCalledWith</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'Could not remind Ben: Mailbox full'</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>A spy replaces a method on a <strong>shared</strong> object. Forget to restore it and every later test in the file runs with a silent <code dir=\"ltr\">console.warn</code>. <code dir=\"ltr\">vi.restoreAllMocks()</code> in <code dir=\"ltr\">afterEach</code>, or <code dir=\"ltr\">restoreMocks: true</code> in the config, makes cleanup automatic.</p>\n<h2 id=\"sec-4\" tabindex=\"-1\">Fake timers: skipping the wait</h2>\n<p>Code that waits (debounced autosave, retry back-off, “undo” toasts) is painful to test in real time. Fake timers replace <code dir=\"ltr\">setTimeout</code>, <code dir=\"ltr\">setInterval</code> and <code dir=\"ltr\">Date</code> with a clock the test controls:</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:#CF222E;--shiki-dark:#FF7B72\">import</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { afterEach, test, expect, vi } </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>\n<span class=\"line\"><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">afterEach</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(() </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\">useRealTimers</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\">'autosaves the draft 2 seconds after the last change'</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:#1F2328;--shiki-dark:#E6EDF3\">  vi.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">useFakeTimers</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\"> save</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\">fn</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\"> draft</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> createDraft</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">({ save, delayMs: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">2000</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> }); </span><span style=\"color:#6E7781;--shiki-dark:#8B949E\">// debounced</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  draft.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">update</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">({ description: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'Din'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> });</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  draft.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">update</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">({ description: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'Dinner'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> });</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  vi.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">advanceTimersByTime</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">1999</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\">(save).not.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">toHaveBeenCalled</span><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\">  vi.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">advanceTimersByTime</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 style=\"color:#8250DF;--shiki-dark:#D2A8FF\">  expect</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(save).</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">toHaveBeenCalledWith</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">({ description: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'Dinner'</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><code dir=\"ltr\">vi.advanceTimersByTime(ms)</code> runs every timer due within that window, instantly. <code dir=\"ltr\">vi.setSystemTime(date)</code> pins <code dir=\"ltr\">new Date()</code>, which is how you test “reminders go out on the first of the month”. When timers schedule promises, use the async variants such as <code dir=\"ltr\">vi.advanceTimersByTimeAsync</code>. (The playground runs real timers only, so this one stays on the page.)</p>\n<h2 id=\"sec-5\" tabindex=\"-1\">When mocking hurts</h2>\n<p>Every mock is a <strong>claim</strong> about how the real thing behaves. When reality drifts from the claim, the test keeps passing. The patterns that bite:</p>\n<ul>\n<li><strong>Mocking your own modules.</strong> Replacing <code dir=\"ltr\">parseAmount</code> inside a form test means the form and parser are never tested together, which was the point of the test. Your own pure code is fast; use it.</li>\n<li><strong>Mocking the thing under test.</strong> If you spy on <code dir=\"ltr\">remindDebtors</code> while testing <code dir=\"ltr\">remindDebtors</code>, you are testing the spy.</li>\n<li><strong>Asserting every call.</strong> Checking the exact sequence of fourteen internal calls freezes the implementation. Assert the calls a caller would care about, like “Ben got a reminder”.</li>\n<li><strong>Mocks that lie.</strong> Your mock API returns <code dir=\"ltr\">{ id }</code>; the real one returns <code dir=\"ltr\">{ data: { id } }</code>. Unit tests green, app broken. The next section’s network mocking and the end-to-end tests in section 4 exist to catch exactly this.</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>Using vi.mock as the first resort</span></p>\n<p><code dir=\"ltr\">vi.mock('./api.js')</code> replaces a whole module for the file, and it is hoisted above your imports, which surprises people. It is useful for a third-party SDK you can’t inject. For your own code, passing the collaborator in as a parameter (as <code dir=\"ltr\">remindDebtors</code> does) keeps tests simpler and the design more honest.</p>\n</aside>\n<p>A quick sanity check for any mock you write: delete one line of the real code it stands in for. If no test fails anywhere in the suite, that mock was hiding the behaviour instead of testing around it, and somewhere you need a test that runs the real thing.</p>\n<p>My rule: <strong>mock at the boundaries</strong> of your system (network, email, payment, time, randomness) and run the real code everywhere inside.</p>\n<p>In the exercise, use <code dir=\"ltr\">vi.fn</code> and <code dir=\"ltr\">vi.spyOn</code> to catch four bugs in <code dir=\"ltr\">remindDebtors</code>. Section 3 moves from functions to the DOM.</p>\n","toc":[{"id":"sec-1","text":"The vocabulary, briefly","level":2},{"id":"sec-2","text":"vi.fn: a function that remembers","level":2},{"id":"sec-3","text":"vi.spyOn: watching a real method","level":2},{"id":"sec-4","text":"Fake timers: skipping the wait","level":2},{"id":"sec-5","text":"When mocking hurts","level":2}],"takeaways":["<code dir=\"ltr\">vi.fn()</code> creates a function that records every call, and you can tell it what to return with <code dir=\"ltr\">mockReturnValue</code>, <code dir=\"ltr\">mockResolvedValue</code> or <code dir=\"ltr\">mockImplementation</code>.","<code dir=\"ltr\">vi.spyOn(object, 'method')</code> watches a real method and can override it; always restore spies, for example with <code dir=\"ltr\">vi.restoreAllMocks()</code> in <code dir=\"ltr\">afterEach</code>.","Mock at the boundaries of your system (network, email, time, randomness) and use the real code for your own modules.","Fake timers (<code dir=\"ltr\">vi.useFakeTimers</code>, <code dir=\"ltr\">vi.advanceTimersByTime</code>) let a test skip ahead through delays instead of waiting for them.","Every mock is a claim about how the real thing behaves; when that claim drifts, the test stays green while production breaks."],"further":[{"title":"Vitest — Mocking guide","url":"https://vitest.dev/guide/mocking"},{"title":"Vitest — Mock functions API","url":"https://vitest.dev/api/mock"},{"title":"Vitest — Fake timers","url":"https://vitest.dev/guide/mocking/timers"}],"quiz":[{"q":"<p>You want to check that <code dir=\"ltr\">remindDebtors</code> calls <code dir=\"ltr\">sendReminder('Ben', 'You owe $5.00 to the group')</code>. Which assertion fits?</p>","options":[{"text":"<code dir=\"ltr\">expect(sendReminder).toHaveBeenCalledWith('Ben', 'You owe $5.00 to the group')</code>","why":"Correct. <code dir=\"ltr\">toHaveBeenCalledWith</code> passes if any recorded call used exactly those arguments, in that order."},{"text":"<code dir=\"ltr\">expect(sendReminder('Ben')).toBe('You owe $5.00 to the group')</code>","why":"This calls the mock yourself and checks its return value; it says nothing about what <code dir=\"ltr\">remindDebtors</code> did."},{"text":"<code dir=\"ltr\">expect(sendReminder).toHaveBeenCalled()</code>","why":"It passes even if the arguments are wrong or swapped, so it would miss most of the bugs you care about."}],"answer":0},{"q":"<p>A test for the expense form mocks the in-house <code dir=\"ltr\">parseAmount</code> module so that it always returns 1250. The real <code dir=\"ltr\">parseAmount</code> gets a bug. What happens to that form test?</p>","options":[{"text":"It fails, because Vitest detects that the mock and the real module disagree.","why":"Vitest has no idea what the real module would return; a mock replaces it completely."},{"text":"It throws, because mocks can’t replace modules you own.","why":"<code dir=\"ltr\">vi.mock</code> can replace any module, yours included, which is exactly the danger here."},{"text":"It stays green, because it never runs the real parser; the form and parser are no longer tested together.","why":"Correct. Mocking your own pure modules buys little speed and removes the integration you wanted to check."}],"answer":2},{"q":"<p>A draft autosaves 2 seconds after the last keystroke. With <code dir=\"ltr\">vi.useFakeTimers()</code> on, how does the test get the save to happen?</p>","options":[{"text":"Wait with <code dir=\"ltr\">await new Promise((r) =&gt; setTimeout(r, 2000))</code>.","why":"Under fake timers that <code dir=\"ltr\">setTimeout</code> is fake too, so it never fires on its own and the test hangs until it times out."},{"text":"Call <code dir=\"ltr\">vi.advanceTimersByTime(2000)</code> after the last keystroke, then assert the save happened.","why":"Correct. Advancing the fake clock runs every timer due within that window, instantly."},{"text":"Set the test timeout to 3 seconds so the real timer has time to fire.","why":"With fake timers installed there is no real timer; a longer timeout only delays the failure."}],"answer":1},{"q":"<p>Why do you call <code dir=\"ltr\">vi.restoreAllMocks()</code> (or set <code dir=\"ltr\">restoreMocks</code>) after tests that use <code dir=\"ltr\">vi.spyOn</code>?</p>","options":[{"text":"To reset the call counts on <code dir=\"ltr\">vi.fn()</code> mocks to zero.","why":"Clearing call history is what <code dir=\"ltr\">mockClear</code> does; restoring is about putting the original method back."},{"text":"To put the original methods back so a spy from one test can’t change behaviour in another.","why":"Correct. A spy replaces a real method on a shared object such as <code dir=\"ltr\">console</code>; without restoring, the override leaks into later tests."},{"text":"Because Vitest refuses to run a second test while a spy exists.","why":"Vitest runs happily with leaked spies, which is exactly why leaks cause confusing failures in unrelated tests."}],"answer":1}],"exercise":{"kind":"web","mode":"test","subject":"function formatMoney(cents) {\n  const sign = cents < 0 ? '-' : '';\n  const abs = Math.abs(cents);\n  return `${sign}$${Math.floor(abs / 100)}.${String(abs % 100).padStart(2, '0')}`;\n}\n\n// Sends a reminder to everyone who owes money (a negative balance).\n// - Calls sendReminder(name, message) once per debtor, with a message\n//   such as \"You owe $5.00 to the group\".\n// - Skips debts smaller than minimumCents (default 100, i.e. $1.00);\n//   a debt of exactly minimumCents IS reminded.\n// - People who are owed money or are settled up get nothing.\n// - If sendReminder throws for someone, logs\n//   console.warn(\"Could not remind <name>: <error message>\")\n//   and carries on with the others.\n// Returns the number of reminders that were sent successfully.\nfunction remindDebtors(balances, sendReminder, { minimumCents = 100 } = {}) {\n  let sent = 0;\n  for (const [name, cents] of Object.entries(balances)) {\n    if (cents >= 0 || -cents < minimumCents) continue;\n    try {\n      sendReminder(name, `You owe ${formatMoney(-cents)} to the group`);\n      sent += 1;\n    } catch (error) {\n      console.warn(`Could not remind ${name}: ${error.message}`);\n    }\n  }\n  return sent;\n}\n","mutants":[{"id":"reminds-creditors","code":"function formatMoney(cents) {\n  const sign = cents < 0 ? '-' : '';\n  const abs = Math.abs(cents);\n  return `${sign}$${Math.floor(abs / 100)}.${String(abs % 100).padStart(2, '0')}`;\n}\n\n// Sends a reminder to everyone who owes money (a negative balance).\n// - Calls sendReminder(name, message) once per debtor, with a message\n//   such as \"You owe $5.00 to the group\".\n// - Skips debts smaller than minimumCents (default 100, i.e. $1.00);\n//   a debt of exactly minimumCents IS reminded.\n// - People who are owed money or are settled up get nothing.\n// - If sendReminder throws for someone, logs\n//   console.warn(\"Could not remind <name>: <error message>\")\n//   and carries on with the others.\n// Returns the number of reminders that were sent successfully.\nfunction remindDebtors(balances, sendReminder, { minimumCents = 100 } = {}) {\n  let sent = 0;\n  for (const [name, cents] of Object.entries(balances)) {\n    if (cents === 0 || Math.abs(cents) < minimumCents) continue;\n    try {\n      sendReminder(name, `You owe ${formatMoney(-cents)} to the group`);\n      sent += 1;\n    } catch (error) {\n      console.warn(`Could not remind ${name}: ${error.message}`);\n    }\n  }\n  return sent;\n}\n"},{"id":"swapped-args","code":"function formatMoney(cents) {\n  const sign = cents < 0 ? '-' : '';\n  const abs = Math.abs(cents);\n  return `${sign}$${Math.floor(abs / 100)}.${String(abs % 100).padStart(2, '0')}`;\n}\n\n// Sends a reminder to everyone who owes money (a negative balance).\n// - Calls sendReminder(name, message) once per debtor, with a message\n//   such as \"You owe $5.00 to the group\".\n// - Skips debts smaller than minimumCents (default 100, i.e. $1.00);\n//   a debt of exactly minimumCents IS reminded.\n// - People who are owed money or are settled up get nothing.\n// - If sendReminder throws for someone, logs\n//   console.warn(\"Could not remind <name>: <error message>\")\n//   and carries on with the others.\n// Returns the number of reminders that were sent successfully.\nfunction remindDebtors(balances, sendReminder, { minimumCents = 100 } = {}) {\n  let sent = 0;\n  for (const [name, cents] of Object.entries(balances)) {\n    if (cents >= 0 || -cents < minimumCents) continue;\n    try {\n      sendReminder(`You owe ${formatMoney(-cents)} to the group`, name);\n      sent += 1;\n    } catch (error) {\n      console.warn(`Could not remind ${name}: ${error.message}`);\n    }\n  }\n  return sent;\n}\n"},{"id":"skips-exact-minimum","code":"function formatMoney(cents) {\n  const sign = cents < 0 ? '-' : '';\n  const abs = Math.abs(cents);\n  return `${sign}$${Math.floor(abs / 100)}.${String(abs % 100).padStart(2, '0')}`;\n}\n\n// Sends a reminder to everyone who owes money (a negative balance).\n// - Calls sendReminder(name, message) once per debtor, with a message\n//   such as \"You owe $5.00 to the group\".\n// - Skips debts smaller than minimumCents (default 100, i.e. $1.00);\n//   a debt of exactly minimumCents IS reminded.\n// - People who are owed money or are settled up get nothing.\n// - If sendReminder throws for someone, logs\n//   console.warn(\"Could not remind <name>: <error message>\")\n//   and carries on with the others.\n// Returns the number of reminders that were sent successfully.\nfunction remindDebtors(balances, sendReminder, { minimumCents = 100 } = {}) {\n  let sent = 0;\n  for (const [name, cents] of Object.entries(balances)) {\n    if (cents >= 0 || -cents <= minimumCents) continue;\n    try {\n      sendReminder(name, `You owe ${formatMoney(-cents)} to the group`);\n      sent += 1;\n    } catch (error) {\n      console.warn(`Could not remind ${name}: ${error.message}`);\n    }\n  }\n  return sent;\n}\n"},{"id":"stops-on-failure","code":"function formatMoney(cents) {\n  const sign = cents < 0 ? '-' : '';\n  const abs = Math.abs(cents);\n  return `${sign}$${Math.floor(abs / 100)}.${String(abs % 100).padStart(2, '0')}`;\n}\n\n// Sends a reminder to everyone who owes money (a negative balance).\n// - Calls sendReminder(name, message) once per debtor, with a message\n//   such as \"You owe $5.00 to the group\".\n// - Skips debts smaller than minimumCents (default 100, i.e. $1.00);\n//   a debt of exactly minimumCents IS reminded.\n// - People who are owed money or are settled up get nothing.\n// - If sendReminder throws for someone, logs\n//   console.warn(\"Could not remind <name>: <error message>\")\n//   and carries on with the others.\n// Returns the number of reminders that were sent successfully.\nfunction remindDebtors(balances, sendReminder, { minimumCents = 100 } = {}) {\n  let sent = 0;\n  for (const [name, cents] of Object.entries(balances)) {\n    if (cents >= 0 || -cents < minimumCents) continue;\n    try {\n      sendReminder(name, `You owe ${formatMoney(-cents)} to the group`);\n      sent += 1;\n    } catch (error) {\n      throw error;\n    }\n  }\n  return sent;\n}\n"}],"starter":{"js":"describe('remindDebtors', () => {\n  let warn;\n\n  beforeEach(() => {\n    // Watch console.warn and keep the test output quiet.\n    warn = vi.spyOn(console, 'warn').mockImplementation(() => {});\n  });\n\n  afterEach(() => {\n    vi.restoreAllMocks(); // puts the real console.warn back\n  });\n\n  test('returns how many reminders were sent', () => {\n    const sendReminder = vi.fn();\n\n    const sent = remindDebtors({ Ana: 1500, Ben: -1500 }, sendReminder);\n\n    expect(sent).toBe(1);\n  });\n\n  // Add tests with vi.fn() and toHaveBeenCalledWith / toHaveBeenCalledTimes.\n});\n"},"solution":{"js":"describe('remindDebtors', () => {\n  let warn;\n\n  beforeEach(() => {\n    warn = vi.spyOn(console, 'warn').mockImplementation(() => {});\n  });\n\n  afterEach(() => {\n    vi.restoreAllMocks(); // puts the real console.warn back\n  });\n\n  test('returns how many reminders were sent', () => {\n    const sendReminder = vi.fn();\n\n    const sent = remindDebtors({ Ana: 1500, Ben: -1500 }, sendReminder);\n\n    expect(sent).toBe(1);\n  });\n\n  test('reminds only debtors, with their name and the amount', () => {\n    const sendReminder = vi.fn();\n\n    remindDebtors({ Ana: 1500, Ben: -500, Cai: 0 }, sendReminder);\n\n    expect(sendReminder).toHaveBeenCalledTimes(1);\n    expect(sendReminder).toHaveBeenCalledWith('Ben', 'You owe $5.00 to the group');\n  });\n\n  test('reminds a debt of exactly the minimum, but not less', () => {\n    const sendReminder = vi.fn();\n\n    remindDebtors({ Ben: -100, Dee: -99, Ana: 199 }, sendReminder);\n\n    expect(sendReminder).toHaveBeenCalledTimes(1);\n    expect(sendReminder).toHaveBeenCalledWith('Ben', 'You owe $1.00 to the group');\n  });\n\n  test('warns and carries on when one reminder fails', () => {\n    const sendReminder = vi.fn().mockImplementation((name) => {\n      if (name === 'Ben') throw new Error('Mailbox full');\n    });\n\n    const sent = remindDebtors({ Ben: -500, Dee: -300, Ana: 800 }, sendReminder);\n\n    expect(sent).toBe(1);\n    expect(sendReminder).toHaveBeenCalledWith('Dee', 'You owe $3.00 to the group');\n    expect(warn).toHaveBeenCalledWith('Could not remind Ben: Mailbox full');\n  });\n});\n"},"checks":[{"id":"passes","suite":"subject"},{"id":"catches-reminds-creditors","suite":"reminds-creditors"},{"id":"catches-swapped-args","suite":"swapped-args"},{"id":"catches-skips-exact-minimum","suite":"skips-exact-minimum"},{"id":"catches-stops-on-failure","suite":"stops-on-failure"}],"title":"Mocks for remindDebtors","prompt":"<p><code dir=\"ltr\">remindDebtors(balances, sendReminder)</code> calls <code dir=\"ltr\">sendReminder(name, message)</code> for each person who owes at least $1.00, warns with <code dir=\"ltr\">console.warn</code> when a reminder fails, and returns how many it sent. Positive balances are owed money; negative balances owe. The rules are in the subject’s comment.</p>\n<p>The starter already spies on <code dir=\"ltr\">console.warn</code> (and restores it after each test). Use <code dir=\"ltr\">vi.fn()</code> for <code dir=\"ltr\">sendReminder</code> and add tests until your suite catches four hidden bugs: one reminds people who are <strong>owed</strong> money, one <strong>swaps</strong> the two arguments, one skips a debt of <strong>exactly</strong> $1.00, and one <strong>stops</strong> at the first failed reminder.</p>\n","hints":["Pass a <code dir=\"ltr\">vi.fn()</code> and check it with <code dir=\"ltr\">toHaveBeenCalledWith('Ben', 'You owe $5.00 to the group')</code> and <code dir=\"ltr\">toHaveBeenCalledTimes(1)</code>. The exact argument order matters.","Boundary: a balance of -100 must be reminded, -99 must not.","For the failure case, make the mock throw for one name: <code dir=\"ltr\">vi.fn().mockImplementation((name) =&gt; { if (name === 'Ben') throw new Error('Mailbox full'); })</code>, then assert that the next debtor was still called and that <code dir=\"ltr\">warn</code> was called with the message."],"explanation":"<p><code dir=\"ltr\">toHaveBeenCalledWith</code> checks the arguments in order, so it catches the swapped version, and <code dir=\"ltr\">toHaveBeenCalledTimes(1)</code> with a creditor in the balances catches the version that reminds everyone. The boundary test uses -100 and -99, one on each side of the limit; -100, exactly on it, is the input that tells <code dir=\"ltr\">&lt;</code> from <code dir=\"ltr\">&lt;=</code>. The failure test makes the mock throw for one person and checks that the <strong>next</strong> person was still reminded; the version that rethrows never gets there and the test fails. The <code dir=\"ltr\">console.warn</code> spy turns a side effect you would normally ignore into something you can assert, and restoring it in <code dir=\"ltr\">afterEach</code> keeps it from leaking into other tests.</p>\n","checkLabels":{"passes":"All your tests pass against the real remindDebtors","catches-reminds-creditors":"Catches the version that reminds people who are owed money","catches-swapped-args":"Catches the version that swaps name and message","catches-skips-exact-minimum":"Catches the version that skips a debt of exactly $1.00","catches-stops-on-failure":"Catches the version that stops at the first failed reminder"}},"words":652,"readMinutes":3}