{"id":"l-tj-5-4","courseId":"testing-javascript","lang":"en","kind":"lesson","summary":"Run a layered test suite in CI with GitHub Actions so every pull request gets fast, trustworthy feedback, and decide which tests to skip or delete because they cost more than they catch.","html":"<p>Tests on your laptop protect you. Tests in CI protect the whole team, on every pull request, whether or not someone remembered to run them. This last lesson puts the pieces together: a pipeline that runs the layers you’ve built in the right order, and the judgement about what deserves a test at all.</p>\n<h2 id=\"sec-1\" tabindex=\"-1\">A pipeline from fast to slow</h2>\n<p>The order matters as much as the content. Cheap checks that fail often go first, so a typo costs 30 seconds of feedback instead of 12 minutes:</p>\n<figure class=\"diagram\"><svg class=\"diagram-svg\" focusable=\"false\" viewBox=\"0 0 680 170\" role=\"img\" aria-labelledby=\"l-tj-5-4-t1\"><defs><marker id=\"arrow-l-tj-5-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-5-4-t1\">Four stages left to right: lint and types in seconds, unit and integration tests in about a minute, build, then sharded end-to-end tests in a few minutes. Each stage runs only if the previous one passed.</title>\n  <rect class=\"d-box-warn\" x=\"10\" y=\"50\" width=\"145\" height=\"70\" rx=\"12\"/>\n  <text class=\"d-label-strong\" x=\"82\" y=\"80\" text-anchor=\"middle\">Lint + types</text>\n  <text class=\"d-label-muted\" x=\"82\" y=\"104\" text-anchor=\"middle\">~30 s</text>\n  <path class=\"d-arrow\" d=\"M155 85 L183 85\" marker-end=\"url(#arrow-l-tj-5-4-1)\"/>\n  <rect class=\"d-box-accent\" x=\"185\" y=\"50\" width=\"145\" height=\"70\" rx=\"12\"/>\n  <text class=\"d-label-strong\" x=\"257\" y=\"80\" text-anchor=\"middle\">Vitest</text>\n  <text class=\"d-label-muted\" x=\"257\" y=\"104\" text-anchor=\"middle\">~1 min</text>\n  <path class=\"d-arrow\" d=\"M330 85 L358 85\" marker-end=\"url(#arrow-l-tj-5-4-1)\"/>\n  <rect class=\"d-box\" x=\"360\" y=\"50\" width=\"120\" height=\"70\" rx=\"12\"/>\n  <text class=\"d-label-strong\" x=\"420\" y=\"80\" text-anchor=\"middle\">Build</text>\n  <text class=\"d-label-muted\" x=\"420\" y=\"104\" text-anchor=\"middle\">~30 s</text>\n  <path class=\"d-arrow\" d=\"M480 85 L508 85\" marker-end=\"url(#arrow-l-tj-5-4-1)\"/>\n  <rect class=\"d-box-primary\" x=\"510\" y=\"50\" width=\"160\" height=\"70\" rx=\"12\"/>\n  <text class=\"d-label-strong\" x=\"590\" y=\"80\" text-anchor=\"middle\">Playwright</text>\n  <text class=\"d-label-muted\" x=\"590\" y=\"104\" text-anchor=\"middle\">3 shards, ~4 min</text>\n  <text class=\"d-label-muted\" x=\"340\" y=\"155\" text-anchor=\"middle\">a failure stops the run early</text>\n</svg><figcaption>A pull request pipeline, cheapest checks first</figcaption></figure>\n<p>Here it is as a GitHub Actions workflow:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"yaml\" data-index=\"0\"><div class=\"code-head\"><span class=\"code-title\">.github/workflows/test.yml</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:#116329;--shiki-dark:#7EE787\">name</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">Test</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">on</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">  pull_request</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">  push</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">    branches</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: [</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">main</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">]</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">jobs</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">  unit</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">    runs-on</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">ubuntu-latest</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">    steps</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">      - </span><span style=\"color:#116329;--shiki-dark:#7EE787\">uses</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">actions/checkout@v6</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">      - </span><span style=\"color:#116329;--shiki-dark:#7EE787\">uses</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">actions/setup-node@v6</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">        with</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">          node-version</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">lts/*</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">          cache</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">npm</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">      - </span><span style=\"color:#116329;--shiki-dark:#7EE787\">run</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">npm ci</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">      - </span><span style=\"color:#116329;--shiki-dark:#7EE787\">run</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">npm run lint</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">      - </span><span style=\"color:#116329;--shiki-dark:#7EE787\">run</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">npx tsc --noEmit</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">      - </span><span style=\"color:#116329;--shiki-dark:#7EE787\">run</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">npx vitest run --coverage</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">  e2e</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">    needs</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">unit</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">    runs-on</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">ubuntu-latest</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">    strategy</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">      fail-fast</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">false</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">      matrix</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">        shard</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 style=\"color:#0550AE;--shiki-dark:#79C0FF\">2</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">3</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">]</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">    steps</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">      - </span><span style=\"color:#116329;--shiki-dark:#7EE787\">uses</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">actions/checkout@v6</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">      - </span><span style=\"color:#116329;--shiki-dark:#7EE787\">uses</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">actions/setup-node@v6</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">        with</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">          node-version</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">lts/*</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">          cache</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">npm</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">      - </span><span style=\"color:#116329;--shiki-dark:#7EE787\">run</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">npm ci</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">      - </span><span style=\"color:#116329;--shiki-dark:#7EE787\">run</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">npx playwright install --with-deps</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">      - </span><span style=\"color:#116329;--shiki-dark:#7EE787\">run</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">npx playwright test --shard=${{ matrix.shard }}/3</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">      - </span><span style=\"color:#116329;--shiki-dark:#7EE787\">uses</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">actions/upload-artifact@v4</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">        if</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">${{ !cancelled() }}</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">        with</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">          name</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">playwright-report-${{ matrix.shard }}</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">          path</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">playwright-report/</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">          retention-days</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">14</span></span></code></pre></div>\n<p>A few details are doing real work:</p>\n<ul>\n<li><strong><code dir=\"ltr\">needs: unit</code></strong> means the end-to-end job starts only when unit tests pass. No point booting browsers for code that fails <code dir=\"ltr\">tsc</code>.</li>\n<li><strong><code dir=\"ltr\">--shard=1/3</code></strong> splits the Playwright suite across three machines that run in parallel. (For one combined report, switch to the <code dir=\"ltr\">blob</code> reporter and run <code dir=\"ltr\">npx playwright merge-reports</code> in a final job.)</li>\n<li><strong><code dir=\"ltr\">if: ${{ !cancelled() }}</code></strong> uploads the report even when tests fail, which is precisely when you need the traces from <a href=\"/lesson/testing-javascript/l-tj-4-3\">the debugging lesson</a>.</li>\n<li><strong><code dir=\"ltr\">CI</code> is set automatically</strong> on GitHub’s runners, so Vitest runs once instead of watching, a stray <code dir=\"ltr\">.only</code> fails, and the Playwright config’s <code dir=\"ltr\">retries: process.env.CI ? 2 : 0</code> kicks in.</li>\n</ul>\n<p>Finally, in the repository settings, make these jobs <strong>required status checks</strong> on <code dir=\"ltr\">main</code>, so a red pipeline actually blocks the merge. A pipeline nobody is required to pass is a suggestion.</p>\n<aside class=\"callout callout-tip\" role=\"note\"><p class=\"callout-title\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><path d=\"M15 14c.2-1 .7-1.7 1.5-2.5 1-.9 1.5-2.2 1.5-3.5A6 6 0 0 0 6 8c0 1 .2 2.2 1.5 3.5.7.7 1.3 1.5 1.5 2.5\"/><path d=\"M9 18h6\"/><path d=\"M10 22h4\"/></svg><span><span class=\"callout-kind\">Tip</span><span class=\"callout-sep\" aria-hidden=\"true\"> · </span>Keep it under ten minutes</span></p>\n<p>People wait for a pipeline that takes five minutes. They context-switch away from one that takes twenty, and they start merging around one that takes forty. Treat pipeline time as a budget: shard, cache dependencies, run Vitest’s fast suite before anything slow, and move checks down the trophy when an end-to-end test duplicates a unit test.</p>\n</aside>\n<h2 id=\"sec-2\" tabindex=\"-1\">Choosing what not to test</h2>\n<p>Every test costs writing time, CI minutes and maintenance. Some cost more than they will ever catch:</p>\n<ul>\n<li><strong>Trivial code</strong>: a getter that returns a field, a constant (<code dir=\"ltr\">expect(CURRENCY).toBe('USD')</code> restates the code).</li>\n<li><strong>Framework and library behaviour</strong>: that <code dir=\"ltr\">useState</code> updates state, that <code dir=\"ltr\">Array.prototype.sort</code> sorts, that your date library formats dates. Their maintainers test that.</li>\n<li><strong>Implementation details</strong>: which private helper was called, how many times a component rendered, internal state. These fail on refactors and pass on bugs.</li>\n<li><strong>Styling</strong>: class names and pixel values in unit tests. If visual regressions matter, use screenshot tests in a real browser for a few key screens.</li>\n<li><strong>Throwaway code</strong>: spikes and prototypes you’re about to delete.</li>\n</ul>\n<p>And be willing to <strong>delete</strong>. A test that has never failed for a real reason in two years, that breaks on every refactor, or that is permanently skipped, is a liability. Deleting it is a contribution.</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>Testing everything at the same level</span></p>\n<p>A suite with 400 end-to-end tests and 20 unit tests is slow, flaky and hard to debug; one with 2,000 unit tests and no end-to-end test can pass while the app can’t even load. Neither is “more testing”. Put each check at the lowest level that can catch the bug, and keep a handful of end-to-end tests for the flows that hold everything together.</p>\n</aside>\n<h2 id=\"sec-3\" tabindex=\"-1\">The whole picture</h2>\n<p>You now have every layer: static checks, unit tests that pin behaviour and boundaries, UI tests that act like users, end-to-end tests that wait correctly and stay isolated, and the tools to judge them, coverage for gaps and mutation testing for strength. The question behind all of it stays the same one from the first lesson: <strong>would this test fail for a bug someone could plausibly ship, quickly, with a message that says what broke?</strong> Keep the tests that answer yes, fix the ones that could, and delete the rest.</p>\n<p>The exercise asks you to make those calls on a real-looking list of tests.</p>\n","toc":[{"id":"sec-1","text":"A pipeline from fast to slow","level":2},{"id":"sec-2","text":"Choosing what not to test","level":2},{"id":"sec-3","text":"The whole picture","level":2}],"takeaways":["Order CI from fast to slow (lint and types, then unit and integration tests, then end-to-end) so cheap failures stop the run early.","Make the test jobs required checks on the main branch, upload Playwright reports as artifacts, and shard slow end-to-end suites across machines.","Keep the whole pipeline fast enough that people wait for it; a 40-minute pipeline gets bypassed.","Don’t test trivial code, framework or library behaviour, styling or implementation details; delete tests that never fail for a real reason.","A test earns its place when it would fail for a bug someone could plausibly ship, quickly and with a clear message."],"further":[{"title":"Playwright — Continuous Integration","url":"https://playwright.dev/docs/ci-intro"},{"title":"Playwright — Sharding","url":"https://playwright.dev/docs/test-sharding"},{"title":"GitHub Docs — About protected branches","url":"https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-protected-branches/about-protected-branches"},{"title":"Vitest — Reporters","url":"https://vitest.dev/guide/reporters"}],"quiz":[{"q":"<p>Your pipeline runs Playwright first (12 minutes), then lint (20 seconds). A typo in an import fails lint on most broken pull requests. What’s the better order?</p>","options":[{"text":"Keep the order; end-to-end tests are the most important, so they should run first.","why":"Importance isn’t the point; running the slowest job first means developers wait 12 minutes to learn about a 20-second problem."},{"text":"Lint and type check first, then unit and integration tests, then end-to-end only if those pass.","why":"Correct. Fast, cheap checks fail early and save runner minutes; the slow stage only runs on code that is already plausible."},{"text":"Run lint only on the main branch after merging.","why":"Then broken code reaches main before anyone notices, which is exactly what CI on pull requests exists to prevent."}],"answer":1},{"q":"<p>Which of these tests is the best candidate for deletion?</p>","options":[{"text":"A regression test for a reported bug where <code dir=\"ltr\">'12.5'</code> was parsed as 1205 cents.","why":"Regression tests for real bugs are among the most valuable tests you own; that bug happened once and can happen again."},{"text":"A test that renders a component and checks that React’s <code dir=\"ltr\">useState</code> setter updates the state.","why":"Correct. It tests React, not your code. React’s own suite covers that, and the test can only fail if React itself is broken."},{"text":"An end-to-end test that adds an expense and checks the balances update.","why":"That’s a critical flow where the connection between UI, API and logic is the risk, exactly what end-to-end tests are for."}],"answer":1},{"q":"<p>The end-to-end suite has grown to 30 minutes on one machine. What’s the most effective first step?</p>","options":[{"text":"Shard it across several machines with <code dir=\"ltr\">--shard</code>, and check whether some end-to-end tests duplicate lower-level ones.","why":"Correct. Sharding cuts wall-clock time immediately; moving duplicated checks down the trophy cuts the total work."},{"text":"Run the end-to-end suite only once a week.","why":"Then a week of changes lands before anyone learns that a critical flow broke, and finding the culprit gets much harder."},{"text":"Increase the timeout of every test so fewer of them fail.","why":"Timeouts aren’t the problem here, and longer ones make a slow suite slower when something does go wrong."}],"answer":0}],"exercise":{"kind":"choice","multiple":true,"options":["page-snapshot","negative-format","usestate","settle-e2e","constant","private-spy","regression","network-404"],"correct":["page-snapshot","usestate","constant","private-spy"],"title":"Keep, or delete?","prompt":"<p>You’ve inherited Splitwise-lite’s test suite. CI takes 25 minutes and the team wants it faster without losing confidence. Select <strong>every test you would delete</strong> (or not write in the first place). Leave the ones that earn their place unselected.</p>\n","hints":["For each test, ask: what plausible bug would make it fail? If the answer is “only a refactor” or “only a bug in React”, it is a candidate.","Regression tests for real bugs and tests of money edge cases are rarely the ones to cut."],"explanation":"<p>Delete four. The whole-page snapshot fails on every markup tweak and teaches people to press “update” without reading. The <code dir=\"ltr\">useState</code> test checks React, not Splitwise-lite. The constant test restates a line of code and can only fail when someone changes the value on purpose. The spy on a private helper fails on refactors and passes on real bugs. Keep the other four: the negative-amount format is a money edge case; the settle-up end-to-end test covers the flow where UI, API and logic meet; the regression test documents a bug that really shipped; and the 404 test covers error handling that users hit on bad links. To speed up CI, combine these deletions with sharding and moving duplicated end-to-end checks down to component tests.</p>\n","checkLabels":{},"optionText":{"page-snapshot":{"text":"<code dir=\"ltr\">expect(container).toMatchSnapshot()</code> on the entire rendered group page (a 400-line snapshot).","why":"Delete. It fails on every harmless markup change and says nothing about which behaviour matters, so reviewers rubber-stamp updates."},"negative-format":{"text":"<code dir=\"ltr\">expect(formatMoney(-505)).toBe('-$5.05')</code>","why":"Keep. Negative amounts and padding are exactly where money formatting breaks, and the test runs in a millisecond."},"usestate":{"text":"A test that renders a component, calls its <code dir=\"ltr\">useState</code> setter through a test hook and checks the state changed.","why":"Delete. It tests React’s own behaviour, which React’s suite already covers; it can’t fail because of your code."},"settle-e2e":{"text":"A Playwright test: add an expense, click Settle up, expect “Everyone is settled up”.","why":"Keep. It is the core flow, and only an end-to-end test checks that the UI, the API and the settle-up logic work together."},"constant":{"text":"<code dir=\"ltr\">expect(DEFAULT_CURRENCY).toBe('USD')</code>","why":"Delete. It restates the source. It only fails when someone changes the constant deliberately, and then it just needs updating too."},"private-spy":{"text":"A test that spies on the internal <code dir=\"ltr\">_sortDebtors</code> helper and asserts it was called exactly once by <code dir=\"ltr\">settleUp</code>.","why":"Delete. It locks in an implementation detail: inlining or renaming the helper breaks it, while a wrong settlement would pass."},"regression":{"text":"A regression test from last spring’s bug report: <code dir=\"ltr\">expect(parseAmount('12.5')).toBe(1250)</code>.","why":"Keep. This bug actually shipped once; the test makes sure it never returns silently."},"network-404":{"text":"A test that <code dir=\"ltr\">loadGroup</code> rejects with “HTTP 404” when the API answers 404.","why":"Keep. Error handling is where bugs hide, and broken or old links make 404s a real user path."}}},"words":633,"readMinutes":3}