Handbooks / CI/CD & DevOps / Chapter 2
Testing in the Pipeline
7 pages · ~5 min✓ Reviewed
Builds on CI/CD Pipelines. Next up: Infrastructure as Code.
Test Pyramid Mental Model
1. The Pyramid
- Unitmany, fast (base)
- Integrationfewer (middle)
- E2Ea few, slow (top)
| Going up | Unit | Integration | E2E |
|---|---|---|---|
| Speed | Fast | Medium | Slow |
| Cost, flakiness | Low | Medium | High |
| Coverage | Narrow | Wider | Broadest |
| Debugging | Easy | Medium | Hard |
2. Fast Feedback Rule
- Push each check to the lowest level that can still catch the bug
- Logic bug: unit test, not a browser run
- Wiring or schema bug: integration test
- Only critical user flows earn an E2E test
3. Shapes and Mistakes
| Shape | Emphasis | Verdict |
|---|---|---|
| Pyramid | Unit base | Healthy |
| Ice-cream cone | Manual or E2E, few units | Slow, brittle |
| Trophy | Static analysis base, integration | Units are thin glue |
Ice-cream cone
Mostly manual or E2E tests with few unit tests gives a slow, brittle suite.
- Why not test everything E2E? Slow, costly, flaky, hard to debug
- When pick the trophy? When units are thin glue code
Unit vs Integration vs End-to-End
1. The Three Levels
- Unit: one function or class, dependencies swapped for mocks, stubs or fakes
- Runs in milliseconds, no network or disk
- Integration: several real components together, e.g. service + real database
- Catches wiring, schema and config bugs
- E2E: whole system through the UI or public API, like a user
- Playwright or Cypress on a login-to-checkout flow
| Unit | Integration | E2E | |
|---|---|---|---|
| Speed | fast | medium | slow |
| Failures | precise | realistic | most realistic |
| How many | most | some | fewest |
- Login
- Add to cart
- Pay
- Confirm
2. Mistake: Over-Mocking
- Everything mocked: tests pass, production breaks
- Asserting internal calls breaks on every refactor
python
assert order.total == 42 # output: good db.save.assert_called_once() # internal: avoid
check what the code returns or does
Assert behaviour
Check behaviour and outputs, not which internal calls were made.
Linting and Static Analysis
1. Linters vs Static Analysis
| Kind | Finds, without running code | Tools |
|---|---|---|
| Linter | style issues, likely bugs | ESLint, Ruff, Pylint, golangci-lint |
| Formatter | layout only | Prettier, Black |
| Type checker | type defects | tsc, mypy |
| Security scanner | taint flows, unsafe patterns | Semgrep, CodeQL, Bandit |
2. Everyday Commands
bash
eslint . --fix ruff check . mypy src/ tsc --noEmit
--fix rewrites files; --noEmit only checks types
3. Where to Run
- Commitpre-commit hook
- Push
- CIlint + types
- Mergeblocked on fail
- Local hooks can be skipped, so CI is the enforcer
- Run the same checks in both places
4. Mistakes
- Blanket-disabling a rule for the whole project
- Ignore comments with no written reason
Fix the cause
Otherwise suppress one line with a reason: # noqa: E501 -- long URL
Code Coverage
1. What It Measures
- Coverage = share of code executed by tests
- Line: lines run. Function: functions called
- Branch: every if/else path, so stricter than line
- One test can hit a line yet skip the other branch
python
def fee(n): if n > 100: n -= 10 return n assert fee(200) == 190
100% line, but the False branch never ran
2. Running It
| Stack | Command |
|---|---|
| Python | pytest --cov=src --cov-branch |
| JavaScript | jest --coverage |
| Go | go test -cover |
- Reports come out as lcov or HTML
- Use the report to find untested paths
3. Limits and Mistakes
- 100% does not prove correctness: executed is not asserted
- Assertion-free tests only chase the number
- Check test strength with mutation testing: Stryker, mutmut
Floor on new code
Use coverage to find gaps, not as a goal. Enforce a floor on new code.
Flaky Tests and Quality Gates
1. Flaky Tests
- Flaky test = passes and fails on the same code
- Eroded trust: people stop believing red builds
| Cause | Fix |
|---|---|
| Timing, sleeps | Wait on a condition, not a sleep |
| Shared state, test order | Isolate state per test |
| Unseeded randomness, real clock | Inject fixed seeds and clocks |
| Real network | Mock external services |
Quarantine, don't retry
Tag the test and track a ticket at once. Auto-retry hides the flake.
2. Quality Gates
- Quality gate = automated pass/fail in CI that blocks merge
- Tests green, lint clean, type check
- Coverage on new code at or above threshold
- No critical vulnerabilities
- Lintseconds
- Types
- Tests
- Coveragenew code
- Scanvulns
- Slow or noisy gates get bypassed
- Run fast checks first; require them on protected branches
- Gate new code, not the whole legacy base
3. Interview Point
- Why not auto-retry flaky tests? It hides the cause and erodes trust.
- What does a gate block? Merge, unless tests, lint, types, coverage pass.
Testing and Quality Cheat Sheet
1. Pyramid and Levels
| Level | Share | Scope |
|---|---|---|
| Unit | ~70% | Isolated and fast |
| Integration | ~20% | Real components together |
| E2E | ~10% | Full user flow, few and critical only |
2. Static Checks and Coverage
- Run static checks in pre-commit and again in CI
bash
eslint . --fix ruff check . mypy src/ tsc --noEmit pytest --cov=src --cov-branch jest --coverage
lint, types, then coverage
- Prefer branch coverage over line coverage
- Gate new code, not the whole legacy base
- Check test strength with mutation testing
3. Flaky Tests and Gates
- No sleeps: wait on conditions
- Isolate state per test
- Seed randomness, fix clocks
- Quarantine flaky tests, never auto-retry
Merge is blocked unless all pass
- 1Tests
- 2Lint
- 3Types
- 4Coverage
- 5Merge
Lowest level
Place each check at the lowest level that catches the bug.
Check yourself
A function has if vip: price *= 0.9 with no else. One test calls it with vip=True. What are line and branch coverage?
- Line coverage: 100%, every line ran
- Branch coverage: 50%, the vip=False path never ran
- Add a vip=False test
A test does sleep(1) then asserts a job finished. It fails 1 run in 10 in CI. What is the fix?
- Timing flake: CI is sometimes slower than 1 second
- Wait on the condition with a timeout, not a fixed sleep
- Auto-retry would only hide it
Every test mocks the database layer and is green, yet a renamed column breaks production. Why?
- Mocks never check the real schema
- Add an integration test against a real database