Intermediate · CI/CD pipelines · topic 28 of 53 · 30 min
Testing, quality gates and artefacts
Explanation
The test pyramid
Tests differ in speed, cost and how much they cover. The test pyramid is a simple guide for the mix: a wide base of small, fast tests and a narrow top of big, slow ones.
| Level | What it checks | Speed | How many |
|---|---|---|---|
| Unit | One function or class on its own, with fakes for databases and APIs | Milliseconds each | Hundreds or thousands |
| Integration | Parts working together: the API with a real test database, a service calling another | Seconds each | Tens to hundreds |
| End-to-end (E2E) | A whole user journey through the real UI, such as "sign up, add to cart, pay" | Many seconds to minutes each | A handful of key journeys |
Before any test runs, linting and static analysis read the code without running it. A linter (ESLint, Ruff, Checkstyle) catches style problems and simple bugs such as unused variables. Static analysis tools (SonarQube, Semgrep, CodeQL) find deeper problems, including some security bugs. They are the cheapest checks, so they run first.
Coverage: a signal, not a goal
Code coverage is the percentage of lines (or branches) that ran during the tests. Low coverage clearly shows untested code. But high coverage does not prove good tests: a test can run every line and check nothing. When a coverage number becomes a target, people write tests that chase the number. Use a sensible floor (for example, do not let coverage drop below the current level) and review what the tests actually assert.
Flaky tests
A flaky test sometimes passes and sometimes fails with no code change, often because of timing, shared test data or calls to real external services. Flaky tests teach the team to ignore red builds. Fix them quickly, or quarantine them: move them out of the blocking suite, with a ticket and an owner, so they do not block everyone while being fixed. Blindly re-running failed tests until they pass hides real bugs.
Quality gates
A quality gate is a set of rules a build must meet to continue: all tests pass, coverage at or above a threshold, zero lint errors, no critical vulnerabilities in dependencies or the image. If any rule fails, the pipeline stops and says why.
Artefacts
An artefact is the output of a build that you deploy or share: a container image, a .jar file, an npm package, a Python wheel, a zipped static site. Two rules matter. Versioned: every artefact has a unique version you can trace back to a commit. Immutable: once published, an artefact never changes; a fix means a new version.
| Kind of artefact | Where it is stored |
|---|---|
| npm package | npmjs.com, GitHub Packages, or a private registry such as Artifactory or Nexus |
| Java .jar | Maven Central or a private Maven repository |
| Python wheel | PyPI or a private index |
| Container image | Docker Hub, GitHub Container Registry, Amazon ECR, Azure Container Registry, Google Artifact Registry |
Tagging images
Tag every image with the git commit SHA, so you always know exactly which code is running. For releases, add semantic version tags. Semantic versioning (semver) is MAJOR.MINOR.PATCH: bump MAJOR for breaking changes, MINOR for new features, PATCH for fixes. Teams often also push moving tags such as 1.4 and 1, which point to the newest 1.4.x and 1.x.y. Never deploy production by the latest tag: it changes under you. An image digest (sha256:...) is the fully immutable name of an image.
GIT_SHA=$(git rev-parse --short HEAD) # for example 9f3c2ab
docker build -t registry.example.com/shop-api:sha-$GIT_SHA .
docker tag registry.example.com/shop-api:sha-$GIT_SHA registry.example.com/shop-api:1.4.2
docker push registry.example.com/shop-api:sha-$GIT_SHA
docker push registry.example.com/shop-api:1.4.2Pipeline speed
- Cache dependencies keyed on the lock file hash, so unchanged dependencies are not downloaded again.
- Run independent jobs in parallel (lint, unit tests, image scan) and split big test suites across several runners.
- Use Docker layer caching: copy the lock file and install dependencies before copying the source code.
- Run only what changed in a large monorepo, using path filters.
Need help?
Stuck on a word or an idea? Search this course, or every course, and jump straight to the explanation or a worked question.
Try:
Watch the lesson
Testing, quality gates and artefacts · 1/7
In this lesson you will learn how tests and quality gates protect a pipeline, and how to store and tag what you build.
Prefer a video by a teacher? Find free videos on “test pyramid ci pipeline quality gates artifacts semantic versioning” on YouTube ↗
In real life: Quality gates at an e-commerce company during sale season
Before a big festive sale, an e-commerce team in Gurugram tightens its pipeline. Every pull request must pass 3,000 unit tests and 150 integration tests, keep line coverage at or above 80%, show zero ESLint errors and have no critical vulnerabilities in the image scan. Five checkout E2E tests run on staging after every merge. A flaky payment-page test is quarantined with an owner and a two-day deadline instead of being re-run until green. Each image is tagged with its git SHA and a semver release tag, so when an issue appears on sale day, the team knows exactly which commit is live and can redeploy the previous tag in minutes.
Practice (0/3 done)
Which kind of test should a healthy test suite have the MOST of?
Write quality_gate(report, min_coverage=80). report is a dict like {"tests_total": 120, "tests_failed": 0, "coverage": 84.5, "critical_vulns": 0, "lint_errors": 0}. Return {"passed": bool, "reasons": [...]}, adding these reason codes in this order: "tests" if any test failed OR no tests ran at all, "coverage" if coverage is below min_coverage, "critical_vulns" if there is at least one critical vulnerability, "lint" if there is at least one lint error. passed is True only when there are no reasons.
Write image_tags(version, sha) that returns the tags to push for a release. Always start with the full version and end with "sha-" plus the first 7 characters of the commit SHA. For a normal release like "1.4.2", also add "1.4" and "1" in between. For a pre-release (a version containing "-", like "2.0.0-rc.1"), add no moving tags.
Quick quiz
Read more (free)
Related help
Before this, learn
This topic builds on these. If something here feels new, open them first.
Related topics
- Deployment strategies, feature flags and GitOpsCI/CD pipelines · same module
- GitHub ActionsCI/CD pipelines · same module
- Testing a full-stack appFull-Stack Development · Performance, security and testing
- Integration tests and CIFlutter and Dart · Responsive design and testing
- Testing basics and the test pyramidKotlin and Android App Development · Testing Android apps
- Testing basics: unit, integration and end-to-endFrontend Web Development · Fast, accessible, findable and safe: quality for real sites
Questions people ask about this
Open a question to see the answer and why. The first ones are this topic's own quiz, so try the quiz above before you peek.
Why is "100% coverage" a poor goal on its own?
Answer: Tests can run every line without checking the results, so the number says little about quality
Coverage shows what ran, not what was verified. Use it as a signal.
What should you do with a test that fails randomly about once in ten runs?
Answer: Fix it soon, or quarantine it with an owner and a ticket
Quarantine keeps the pipeline trustworthy while someone fixes the real cause.
What does "immutable artefact" mean?
Answer: Once version 1.4.2 is published, its content never changes
A fix means a new version, so a version number always means one exact build.
Under semantic versioning, a release adds a new feature without breaking anything. Version 2.3.1 becomes:
Answer: 2.4.0
A new backward-compatible feature bumps MINOR and resets PATCH to 0.
In the testing pyramid, which kind of test should you have the most of?Related
Answer: Unit tests
Unit tests are fast and cheap, so you can have hundreds. End-to-end tests are slow, so keep only a few key flows.
Which binding must integration tests initialise?Related
Answer: IntegrationTestWidgetsFlutterBinding
Call ensureInitialized() first.
What does the test pyramid recommend?Related
Answer: Mostly unit tests, fewer integration tests, few UI tests
Fast, focused tests form the base.
In the test pyramid, which kind of test should you have the most of?Related
Answer: Unit
Unit tests are fast and cheap, so you can have many. E2E tests are slower, so keep a few for key journeys.
Pass the quiz (60% or more) or mark this topic as done when you are ready.
Your progress is saved on this device only.
Next steps
- 1 more topic in “CI/CD pipelines” before its exam: Deployment strategies, feature flags and GitOps.
- Module 6 exam: CI/CD pipelines · 12 questions · 20 min, without help. 5 topics of this module still to finish first.
- Then the next module, Infrastructure as code with Terraform. 53 topics left in the course before the final exam.