Testing strategy inside the pipeline
Layer unit, integration and end-to-end tests, use service containers for real dependencies, handle flaky tests honestly, split and parallelise, and publish results.
Which test runs when
| Layer | Runs on | Typical duration | Blocks a merge? |
|---|---|---|---|
| Lint and type check | Every commit | Under a minute | Yes |
| Unit | Every commit | A few minutes | Yes |
| Integration | Every commit | More if containers start | Yes, if kept small |
| End-to-end | Default branch, pre-release | Ten minutes or more | No, but alert |
| Performance and soak | Nightly or weekly | Long | No, tracked as a trend |
jobs:
unit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 22, cache: npm }
- run: npm ci
- run: npm run lint
- run: npm run test:unit -- --reporter=junit --output=reports/unit.xml
- if: always()
uses: actions/upload-artifact@v4
with:
name: unit-results
path: reports/
integration:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16
env:
POSTGRES_PASSWORD: ci
ports: ['5432:5432']
options: >-
--health-cmd "pg_isready -U postgres"
--health-interval 5s --health-timeout 5s --health-retries 10
redis:
image: redis:7
ports: ['6379:6379']
env:
DATABASE_URL: postgres://postgres:ci@localhost:5432/postgres
REDIS_URL: redis://localhost:6379
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npm run migrate
- run: npm run test:integration- A service container needs a health check, otherwise the first test connects before the database is accepting connections.
- Use a real database engine in integration tests. An in-memory substitute with different semantics lets bugs through.
- Each job gets a clean database; sharing one across parallel test files creates ordering dependencies that surface as random failures.
- Upload results with
if: always(), or a failing suite produces no report at exactly the moment you need one.
Flaky tests
# retry at the job level, not inside the test, so retries are visible
test:e2e:
runs-on: ubuntu-latest
continue-on-error: false
strategy:
fail-fast: false
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npm run test:e2e
continue-on-error: true # first attempt
- id: retry
if: steps.first.outcome == 'failure'
run: npm run test:e2e
# publish the JUnit report so the flaky test is named, not buried
- if: always()
uses: actions/upload-artifact@v4
with:
name: e2e-results
path: reports/e2e.xml⚠️
A retry that hides a flaky test is how a suite becomes untrustworthy. Track the failure rate per test and fix or delete anything above a threshold — a test that fails ten percent of the time trains the team to ignore red.
Splitting and reporting
# split by timing so each shard takes a similar time
npx jest --shard=1/4 --reporters=jest-junit
npx jest --shard=2/4 --reporters=jest-junit
# pytest: distribute by test duration from the previous run
pytest -n auto --dist loadfile --junitxml=reports/junit.xml
# once, record durations so the split is informed rather than arbitrary
pytest --store-durations --durations-path=.test_durations| Split strategy | Balances by | Needs |
|---|---|---|
| Alphabetical | Nothing useful | No setup |
| File count | Number of files | Consistent file size |
| Previous duration | Real time | Stored timing data |
| Changed files | Only affected tests | Dependency graph |
Run only the tests affected by a change once the suite is large. It requires a reliable dependency graph, so start with splitting by duration and add affected-only runs when the graph is trustworthy.
FAQ
Why does the same job pass locally and fail in CI?
Usually timezone, locale, filesystem case sensitivity, or parallelism. CI runs in UTC on a case-sensitive filesystem, and tests that share state behave differently when run concurrently.
Should end-to-end tests block a merge?
Only a small smoke subset, and only if it is genuinely reliable. A long flaky end-to-end suite in the merge path is the fastest way to make people bypass the pipeline.
Related
Triggers, concurrency and matrix builds Observability and speeding up slow pipelines
Last refreshed 2026-09-18.