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.

CI/CD & DevOpsPage 1

Test Pyramid Mental Model

1. The Pyramid

  1. Unitmany, fast (base)
  2. Integrationfewer (middle)
  3. E2Ea few, slow (top)
Going upUnitIntegrationE2E
SpeedFastMediumSlow
Cost, flakinessLowMediumHigh
CoverageNarrowWiderBroadest
DebuggingEasyMediumHard

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

ShapeEmphasisVerdict
PyramidUnit baseHealthy
Ice-cream coneManual or E2E, few unitsSlow, brittle
TrophyStatic analysis base, integrationUnits 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
CI/CD & DevOpsPage 2

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
UnitIntegrationE2E
Speedfastmediumslow
Failurespreciserealisticmost realistic
How manymostsomefewest
  1. Login
  2. Add to cart
  3. Pay
  4. 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.

CI/CD & DevOpsPage 3

Linting and Static Analysis

1. Linters vs Static Analysis

KindFinds, without running codeTools
Linterstyle issues, likely bugsESLint, Ruff, Pylint, golangci-lint
Formatterlayout onlyPrettier, Black
Type checkertype defectstsc, mypy
Security scannertaint flows, unsafe patternsSemgrep, 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

  1. Commitpre-commit hook
  2. Push
  3. CIlint + types
  4. 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

CI/CD & DevOpsPage 4

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

StackCommand
Pythonpytest --cov=src --cov-branch
JavaScriptjest --coverage
Gogo 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.

CI/CD & DevOpsPage 5

Flaky Tests and Quality Gates

1. Flaky Tests

  • Flaky test = passes and fails on the same code
  • Eroded trust: people stop believing red builds
CauseFix
Timing, sleepsWait on a condition, not a sleep
Shared state, test orderIsolate state per test
Unseeded randomness, real clockInject fixed seeds and clocks
Real networkMock 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
  1. Lintseconds
  2. Types
  3. Tests
  4. Coveragenew code
  5. 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.
CI/CD & DevOpsPage 6

Testing and Quality Cheat Sheet

1. Pyramid and Levels

LevelShareScope
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
  1. 1Tests
  2. 2Lint
  3. 3Types
  4. 4Coverage
  5. 5Merge
Lowest level

Place each check at the lowest level that catches the bug.

CI/CD & DevOpsPage 7

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