Section 5 · Testing Strategy
Testing in CI and Choosing What Not to Test
18 min3 min readChoose wisely
Lesson at a glance
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.
No video here — just the real thing: a written lesson, runnable examples, a quiz and practice.
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.
A pipeline from fast to slow
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:
Here it is as a GitHub Actions workflow:
name: Test
on:
pull_request:
push:
branches: [main]
jobs:
unit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version: lts/*
cache: npm
- run: npm ci
- run: npm run lint
- run: npx tsc --noEmit
- run: npx vitest run --coverage
e2e:
needs: unit
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
shard: [1, 2, 3]
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version: lts/*
cache: npm
- run: npm ci
- run: npx playwright install --with-deps
- run: npx playwright test --shard=${{ matrix.shard }}/3
- uses: actions/upload-artifact@v4
if: ${{ !cancelled() }}
with:
name: playwright-report-${{ matrix.shard }}
path: playwright-report/
retention-days: 14A few details are doing real work:
needs: unitmeans the end-to-end job starts only when unit tests pass. No point booting browsers for code that failstsc.--shard=1/3splits the Playwright suite across three machines that run in parallel. (For one combined report, switch to theblobreporter and runnpx playwright merge-reportsin a final job.)if: ${{ !cancelled() }}uploads the report even when tests fail, which is precisely when you need the traces from the debugging lesson.CIis set automatically on GitHub’s runners, so Vitest runs once instead of watching, a stray.onlyfails, and the Playwright config’sretries: process.env.CI ? 2 : 0kicks in.
Finally, in the repository settings, make these jobs required status checks on main, so a red pipeline actually blocks the merge. A pipeline nobody is required to pass is a suggestion.
Choosing what not to test
Every test costs writing time, CI minutes and maintenance. Some cost more than they will ever catch:
- Trivial code: a getter that returns a field, a constant (
expect(CURRENCY).toBe('USD')restates the code). - Framework and library behaviour: that
useStateupdates state, thatArray.prototype.sortsorts, that your date library formats dates. Their maintainers test that. - Implementation details: which private helper was called, how many times a component rendered, internal state. These fail on refactors and pass on bugs.
- Styling: class names and pixel values in unit tests. If visual regressions matter, use screenshot tests in a real browser for a few key screens.
- Throwaway code: spikes and prototypes you’re about to delete.
And be willing to delete. 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.
The whole picture
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: would this test fail for a bug someone could plausibly ship, quickly, with a message that says what broke? Keep the tests that answer yes, fix the ones that could, and delete the rest.
The exercise asks you to make those calls on a real-looking list of tests.
Key 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.