Skip to main content
RunBook Academy

Git, CI/CD & GitOpsCXII · Container Delivery PipelineSourceToTest

Source to test — lint, unit, integration

Advanced⏱ ~26 mingitdocker

What you'll learn

  • Order the test stages from cheapest to most realistic and explain why the order matters
  • Run a lint stage as a fast fail filter that catches formatting and obvious bugs before tests start
  • Run unit tests against the application source, not the image, to minimise the cost of a fix
  • Run integration tests against a built image to validate the image as a unit before sign and push

Prerequisites

Verified against Git 2.55.x teaching target; 2.40+ minimum · GitHub Actions continuous service; Aug 2026 documentation baseline · Argo CD v3.5.x teaching target; v3.0+ minimum · Flux v2.9.x · Sigstore Cosign v3.1.x · SLSA v1.2 · OCI Distribution Specification v1.1 · Git LFS v3.7.1 · Kubernetes (cross-course target) 1.36.x

Not yet marked complete on this device.

The first stage of the container delivery pipeline takes a source commit and produces a green test result. Three kinds of test participate, ordered from cheapest to most realistic: lint, unit, integration. The order is not stylistic; it is a feedback loop optimisation. Lint returns in seconds. Unit tests return in tens of seconds. Integration tests return in minutes. Running them in the wrong order puts the slowest filter in front of the fastest.

The test pyramid applied to container delivery

flowchart LR
    A["Source commit"] --> B["Lint"]
    B --> C["Unit tests"]
    C --> D["Integration tests"]
    D --> E["Green build"]

Lint is a static analysis of the source and the Dockerfile. It catches formatting drift, dead imports, obvious bugs, and Dockerfile anti-patterns (running as root, unpinned tags, secret leaks in ENV). Lint does not execute the application; it reads.

Unit tests exercise the application source in isolation. They mock the database, the cache, the message bus. A unit test that talks to a real database is not a unit test; it is a slow, unreliable integration test with extra steps.

Integration tests exercise the built image as a unit. The image is started (locally, in a sidecar, or in a disposable environment), the application is exercised through its real network interface, and the result is asserted. Integration tests are the closest the pipeline gets to production without being production.

Lint as a fast fail filter

The Dockerfile is linted alongside the source. hadolint is the dominant Dockerfile linter:

hadolint Dockerfile

Source linters are language-specific: golangci-lint for Go, ruff for Python, eslint for JavaScript, shellcheck for shell. Each catches the cheap class of mistakes that are embarrassing in code review and expensive to debug at runtime.

The production pattern: lint runs first, fails fast, and never blocks on a network call. A commit with a formatting failure is a commit that should not consume unit-test minutes.

Unit tests against source

Unit tests run against the source tree at ${COMMIT_SHA}. The typical invocation is a one-liner that the language’s test runner already provides:

go test ./...
npm test
pytest -q

The unit-test stage has three properties worth pinning:

  • No network. A unit test that downloads a dependency at runtime is a unit test that will break when the registry is down. Dependencies are vendored or pre-installed in the runner image.
  • No side effects. A unit test that writes to the runner’s filesystem is a unit test that will fail when two unit tests run in parallel and clobber each other.
  • Deterministic. A unit test that produces different results on different runs is a unit test that produces no signal. Mocks and fixtures are the determinism mechanism.

Integration tests against the built image

Integration tests run after the build, against the image the build produced. The pipeline cannot start the image before the image exists; integration tests wait for the build to produce a digest. Once the digest exists, the test harness starts the container, waits for the health check, and exercises the application through its real interface:

docker run --detach --name app-under-test app:$COMMIT_SHA
sleep 5
curl --fail http://localhost:8080/healthz
docker stop app-under-test

The harness is short-lived: start, exercise, stop. The image under test is the same image that will be signed and pushed, so the integration test result is a property of that image, not of a hypothetical one.

What this stage does not do

The source-to-test stage produces a green-or-red signal and a test report. It does not:

  • Build the image. The next stage does that.
  • Push anything. No image, no signature, no SBOM is published by this stage.
  • Decide whether the image is correct. The tests answer the question they are written to answer; they do not answer the question a security auditor or a customer will ask.
  • Run end-to-end tests. End-to-end tests are a later stage that exercises the cluster, the database, and the network in combination. This stage is about the image and the source.

Production discipline

  1. Lint first, fail fast. A formatting bug caught at the lint stage is caught in milliseconds; caught at the integration stage, it consumes minutes.
  2. Pin test commands to tool versions. A go test invoked without a pinned Go version is a test that depends on the runner image’s Go version. Pin both.
  3. Run unit tests in parallel, integration tests serially. A unit test that depends on another unit test’s side effect is a unit test that does not parallelise. Integration tests that start the same image twice step on each other; run them one at a time.
  4. Treat a green test as a property of a specific digest. A green test report that cannot name the image it ran against is a report that cannot be audited. Include the digest in the test report metadata.

Cross-course references

  • This course, Part CII (RepositoryEngineering) - lessons on repository layout and module boundaries inform how unit tests are scoped.
  • This course, Part CIV (RunnerHygiene) - covers the runner environment that lint and unit tests run in.
  • Containers for Production Sysadmins - Part XI covers image testing in CI, which is the integration-test side of this lesson.

Quiz

Knowledge check · 4 questions

  1. Q1. Which ordering of the test stages is correct, and why?

  2. Q2. Integration tests in the container delivery pipeline should run against the image the build produced, identified by digest.

  3. Q3. Name the three properties of a unit test that make it cheap enough to run on every commit.

  4. Q4. Diagnose a pipeline where unit tests are slow and integration tests are flaky, and prescribe the fix.

    A team's container pipeline runs in this order: integration tests, then unit tests, then lint. Integration tests take 18 minutes; unit tests take 14 minutes; lint takes 2 seconds. The integration tests start the application image, then attempt to reach an internal staging API at https://staging.internal/api. Roughly 30% of integration runs fail with 'connection refused' because the runner is on a network that cannot reach staging.internal. Engineers ignore the failures most of the time; the real signal is buried.

Passing score: 75%. Answers are checked in this browser.