A test that nobody runs catches no bugs. CI/CD is what turns your test suite from a sporadic local exercise into an always-on safety net. API tests are particularly well-suited to CI: no browser, no rendering, no flaky clicks — just HTTP requests and assertions. This lesson covers how to wire API tests into a real pipeline, how to tier them so the suite stays fast, and the operational details (secrets, environments, reporting) that turn a working pipeline into a pleasant one to maintain.
Where API tests fit in the pipeline
Most teams end up with three or four trigger points:
Step 1 of 6
PR opened or updated
Lint, unit tests, then API smoke + functional tests against an ephemeral or shared test environment. Goal: 5–10 minutes to a result.
Different layers, different frequencies, different runtimes. The PR check stays fast; the nightly run gets to be slow.
Running tests in CI
The recipe for any CI provider is the same:
- Check out the code.
- Install the test framework's dependencies.
- Either start the application locally or point at a deployed environment.
- Run the test command.
- Upload the report as an artefact.
- Post status back to the PR / Slack.
A representative GitHub Actions workflow:
name: API tests
on: [pull_request]
jobs:
api-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with: { python-version: "3.12" }
- run: pip install -r requirements.txt
- name: Run smoke tests
run: pytest tests/smoke -v --junit-xml=smoke.xml
- name: Run functional tests
run: pytest tests/functional -v --junit-xml=functional.xml -n 4
env:
API_BASE_URL: ${{ secrets.STAGING_URL }}
API_TOKEN: ${{ secrets.STAGING_TOKEN }}
- name: Upload reports
if: always()
uses: actions/upload-artifact@v4
with: { name: test-reports, path: "*.xml" }Every popular CI does the same shape: install, configure, run, report. Pick the one your team uses; the YAML differs but the structure doesn't.
Where the API runs
You have three options for what your tests hit:
- App started fresh in CI. Spin the application up in the same job (often via Docker Compose), wait for it to be ready, run tests against
localhost. Most isolated; slowest startup. - Deployed test environment. Tests point at a URL like
https://staging.example.com. Fast; couples test reliability to environment availability. - Production (carefully). Read-only smoke tests that prove the deployed system answers correctly. Never destructive, never data-modifying.
A common combination: app-started-in-CI for full functional tests, deployed-staging for nightly integration tests, production for post-deploy smoke checks.
Parallel execution
API tests are usually stateless once you have good test data hygiene (Lesson 2 of this chapter). Parallel execution becomes free speed:
pytest -n 8 # 8 workers
mocha --parallel # JS
maven -T 4 # 4 threadsThe constraints:
- Tests must use unique data (UUIDs, timestamps) — covered in the data lesson.
- Shared resources (rate limits, single-tenant environments) become the bottleneck.
- Some integration suites can't parallelise — accept it and run them serially in their own stage.
A 50-test suite that takes 8 minutes serially often takes 90 seconds in parallel. That difference compounds over hundreds of CI runs per week.
Fail fast
One of the highest-ROI optimisations: order your CI steps so cheap ones run first.
1. Lint (5s)
2. Unit tests (30s)
3. Smoke tests (60s) ← if these fail, skip the rest
4. Functional tests (5m)
5. Contract tests (3m)
If smoke fails, there's no point spending 8 minutes on functional and contract tests against a broken build. Most CI platforms support stage-level conditional execution; use it.
Reporting
A test result that nobody sees is wasted effort. Three reporting mechanisms worth setting up:
- JUnit XML. The lingua franca of test reports. Every CI platform displays it, every dashboard tool consumes it. Your test framework almost certainly emits it (
pytest --junitxml=...,mocha --reporter mocha-junit-reporter, JUnit's built-in). - HTML reports. Useful artefacts for triage. Tools like
pytest-html, Allure, or ReportPortal generate browsable HTML you can attach to a build. - Slack / Teams notifications. Post on failure (or on every red main-branch run). Include the failing test names, the build link, and the commit author.
Skip these and the team's relationship with testing erodes. People stop trusting CI when failures don't reach them.
Secrets and configuration
Three rules:
- Never commit secrets. Tokens, passwords, API keys go into CI's secret store (GitHub Secrets, Jenkins credentials, GitLab CI variables). They're injected as environment variables at runtime.
- Minimum scope. Test tokens should have only the permissions tests need. A leaked test token shouldn't be able to delete production data.
- Rotate. Test credentials should rotate periodically — especially if a team member leaves, or if a credential might have leaked.
Practically:
env:
API_BASE_URL: ${{ secrets.STAGING_URL }}
API_TOKEN: ${{ secrets.TEST_API_TOKEN }}The test code reads os.environ["API_TOKEN"] and never knows the actual value.
Environment management
Two axes to think about:
- Per-PR ephemeral environments. Spin up a fresh app + DB for each PR; tear down on close. Strongest isolation, highest infra cost. Increasingly common in well-resourced teams.
- Shared staging. One environment for everyone. Cheaper but contention is real — flaky failures from "another team's data is in the way" become a recurring source of pain.
The pragmatic middle: per-PR ephemeral for the app (cheap to spin up), shared staging for downstream services (impractical to copy). Combine with good test data hygiene to minimise contention.
Monitoring CI itself
A few signals worth tracking:
- Pass rate. If main is green 60% of the time, the suite isn't trustworthy. Aim for ≥ 95% green on main.
- Mean time to result. PR opened → CI verdict. The faster, the more developers run tests locally too.
- Flake rate. Tests that pass-then-fail on the same code. Each flake is a tiny tax on team trust; track them and pay them down.
- Most-failing tests. A small handful of tests usually account for the majority of failures. Focusing on them buys the biggest stability win.
A weekly review meeting on these four metrics keeps the test suite a tool, not a chore.
When CI breaks
The first time a flake costs an hour, the team writes a postmortem. The hundredth time, they shrug and "rerun CI." Stay vigilant:
- Quarantine flaky tests with a tag (
@flaky) — they run separately, don't block PRs, and have a known investigation queue. - Fail loud on infrastructure issues — distinguish "the test failed" from "the CI environment broke." Different fixes.
- Have a "rerun CI" conversation rule. The first rerun is fine; the second triggers an investigation. Otherwise the suite slowly rots.
⚠️ Common mistakes
- Putting all tests on every PR. A 30-minute PR check is a 30-minute disincentive to do anything. Tier ruthlessly.
- Hardcoding the test URL. Make it an env var. The same suite should be runnable against local, CI ephemeral, and staging without code changes.
- No timeouts on requests. A hung downstream call hangs the entire CI job until the platform's overall timeout fires. Always set per-request timeouts and hard kill at suite level.
🎯 Practice task
Wire up a CI run for an API test. 45-60 minutes.
- In your repo, create a minimal API test (one happy-path GET) using your team's framework.
- Set up a CI workflow file (GitHub Actions, GitLab CI, Jenkins) that installs deps, runs the test, and uploads the JUnit report.
- Configure two secrets:
API_BASE_URLandAPI_TOKEN. Reference them in the workflow. - Push a branch and open a PR. Watch the CI run. Confirm the report is attached to the PR.
- Deliberately break the test (assert on the wrong status). Push. Confirm the PR shows red and the failure detail is accessible.
- Add a second job that runs only on
push: branches: [main]for a slower nightly suite skeleton. - Stretch: add a Slack notification step (most CI platforms have a community action). Post on failure with a link back to the build.
That wraps up Chapter 8 — and the theoretical core of the course. Chapter 9 is the capstone: a hands-on project that exercises the entire toolkit you've built.