EduFlow
Testing JavaScript Apps
0%

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.

  1. 1A pipeline from fast to slow
  2. 2Choosing what not to test
  3. 3The whole picture

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:

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. Lint + types ~30 s Vitest ~1 min Build ~30 s Playwright 3 shards, ~4 min a failure stops the run early
A pull request pipeline, cheapest checks first

Here it is as a GitHub Actions workflow:

.github/workflows/test.yml
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: 14

A few details are doing real work:

  • needs: unit means the end-to-end job starts only when unit tests pass. No point booting browsers for code that fails tsc.
  • --shard=1/3 splits the Playwright suite across three machines that run in parallel. (For one combined report, switch to the blob reporter and run npx playwright merge-reports in a final job.)
  • if: ${{ !cancelled() }} uploads the report even when tests fail, which is precisely when you need the traces from the debugging lesson.
  • CI is set automatically on GitHub’s runners, so Vitest runs once instead of watching, a stray .only fails, and the Playwright config’s retries: process.env.CI ? 2 : 0 kicks 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 useState updates state, that Array.prototype.sort sorts, 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.

Further reading

Check your understanding

3 questions

Question 1 of 3

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?

Question 2 of 3

Which of these tests is the best candidate for deletion?

Question 3 of 3

The end-to-end suite has grown to 30 minutes on one machine. What’s the most effective first step?

Practice

Choose wisely

Keep, or delete?

You’ve inherited Splitwise-lite’s test suite. CI takes 25 minutes and the team wants it faster without losing confidence. Select every test you would delete (or not write in the first place). Leave the ones that earn their place unselected.

Keep, or delete?