DevOpsLesson
DevOpsLesson

Free, comprehensive DevOps tutorials and learning roadmaps. Master Docker, Kubernetes, CI/CD, and more.

Stay Updated

Get notified about new tutorials and features.

Tutorials

  • What is DevOps?
  • Docker Tutorial
  • Terraform Tutorial
  • CI/CD Pipeline
  • All Tutorials

Roadmaps

  • DevOps Engineer
  • Cloud Engineer
  • SRE Path
  • All Roadmaps

Company

  • About Us
  • Blog
  • Contact
  • Privacy Policy
  • Terms of Service

© 2026 DevOpsLesson. All rights reserved.

DOCKERKUBERNETESTERRAFORMAWSCI/CDLINUXGITDEVOPS ROADMAPCLOUD ROADMAPSRE ROADMAPGIT CHEATSHEETDOCKER CHEATSHEETK8S CHEATSHEETTF CHEATSHEETLINUX CHEATSHEETDOCKERFILE LINTERYAML VALIDATORCRON PARSERREGEX TESTER

Cicd Tutorial

Introduction to CI/CD
Getting Started with GitLab CI/CD
Stages, Jobs, Artifacts & Cache
Variables, Secrets and Environments in GitLab CI
GitLab Runners
GitLab CI Pipeline Rules
GitLab CI Environments
GitLab CI Docker Workflows
GitLab CI Testing
GitLab CI Security Scanning
GitLab CI Deploy to AWS
GitLab CI Multi-Project Pipelines
GitLab CI Best Practices

GitLab CI Testing

PreviousPrev
Next

Learn GitLab CI testing, unit tests, JUnit reports, code coverage, coverage badges, cobertura visualization, parallel test execution, CI test variables, and a complete Jest pipeline example.

Automated testing is not a “nice to have” in CI/CD. It is the control system that tells you whether a change is safe to merge, safe to release, and safe to trust. Without automated tests in your pipeline, every merge becomes a guess and every deployment becomes a gamble. That is why GitLab CI testing, GitLab CI unit tests, GitLab CI test reports, and GitLab CI code coverage are core skills for any DevOps engineer or software team.

In GitLab, testing is not just about running a command and getting a pass or fail result. GitLab can also capture structured JUnit XML reports, display test summaries in merge requests, extract coverage percentages from logs, visualize line-level coverage changes, publish badges, and even parallelize test execution to keep feedback fast.

Why Automated Testing in CI Is Non-Negotiable

Local testing is helpful, but it is not enough. Developers have different operating systems, tool versions, environment variables, and local habits. CI gives you one consistent place where every important change is validated the same way.

Automated testing in GitLab CI matters because it helps you:

  • catch bugs before merge or deployment
  • prevent regressions from sneaking into main
  • provide fast feedback to contributors
  • create confidence for refactoring and release automation
  • expose test history and visibility to the whole team

A pipeline without testing may still look automated, but it is missing a key safety layer.

Running Unit Tests in a GitLab CI Job

At its simplest, a test job is just a normal CI job that runs your test command.

Node.js and Jest example

unit-tests:
  stage: test
  image: node:20
  script:
    - npm ci
    - npm test

This is the foundational pattern for GitLab CI Jest pipelines.

Python and pytest example

unit-tests:
  stage: test
  image: python:3.12
  script:
    - pip install -r requirements.txt
    - pytest

This is the starting point for GitLab CI pytest workflows.

The language does not matter as much as the principle: every push or merge request should run the relevant automated tests in a clean environment.

Collecting JUnit XML Reports

A plain pass or fail result is useful, but GitLab can do much more when you upload test results in JUnit XML format. This is done with artifacts:reports:junit.

unit-tests:
  stage: test
  image: node:20
  script:
    - npm ci
    - npm test -- --ci --reporters=default --reporters=jest-junit
  artifacts:
    when: always
    reports:
      junit: junit.xml

Why JUnit reports matter

When GitLab understands your test results as structured data, it can:

  • show failed test cases in the UI
  • summarize test counts in merge requests
  • compare results across pipeline runs
  • make debugging easier than scanning raw logs

This is one of the most valuable upgrades from “basic CI” to “developer-friendly CI.”

Why when: always is helpful

If tests fail, you usually still want the JUnit report. Setting when: always ensures GitLab uploads the report even when the job exits with a non-zero status.

Viewing Test Results in the Merge Request UI

After you upload a GitLab CI JUnit report, GitLab can display a test summary widget in the merge request UI. Reviewers can often see:

  • how many tests passed
  • how many failed
  • which specific test cases failed
  • whether the MR introduced new failures

This shortens feedback loops dramatically. Instead of opening raw job logs and scrolling for minutes, developers get structured information in the place where code review already happens.

For busy teams, this alone is a huge quality-of-life improvement.

Code Coverage: What It Measures

Code coverage describes how much of your application code is exercised by your tests. It is usually expressed as a percentage.

Coverage can be measured in different ways, such as:

  • line coverage
  • branch coverage
  • function coverage
  • statement coverage

In GitLab CI, the most common beginner use case is to extract a coverage percentage from test output and show it in the pipeline or merge request.

Coverage is not the same as quality. High coverage does not automatically mean good tests. But low or absent coverage is often a warning sign that important paths are untested.

Extracting Coverage Percentage with coverage Regex

GitLab can parse a percentage from the job log using the coverage keyword.

unit-tests:
  stage: test
  image: node:20
  script:
    - npm ci
    - npm test -- --coverage
  coverage: '/All files[^|]*\|[^|]*\s+([0-9]+\.?[0-9]*)%/'

This tells GitLab to look for a matching percentage in the output.

Why regex-based coverage exists

Different test tools print coverage in different formats. GitLab therefore lets you provide a regex suited to your tool.

Examples:

  • Jest prints a tabular summary
  • pytest with coverage plugins may print another format
  • Java tools such as JaCoCo use their own style

The point is not the exact regex syntax. The point is to make the percentage visible in GitLab so coverage becomes part of daily development.

Coverage Visualization in Merge Request Diffs

A single total percentage is useful, but line-level visualization is even better. GitLab supports coverage visualization in merge requests using report formats such as Cobertura.

That usually looks like this:

artifacts:
  reports:
    coverage_report:
      coverage_format: cobertura
      path: coverage/cobertura-coverage.xml

With a proper coverage report, GitLab can highlight covered and uncovered lines directly in the MR diff. This helps reviewers answer a practical question: did the new code actually get tested?

That is much more actionable than a single percentage alone.

Coverage Badges for README Files

Many teams like to display a GitLab CI coverage badge in their README. A badge makes test status and coverage visible at a glance.

Why use badges?

  • they reinforce quality visibility
  • they help internal consumers assess repository health quickly
  • they create a public signal in open source projects

Badges should not become vanity metrics, but they can be a useful indicator when backed by real testing discipline.

Failing the Pipeline on Coverage Drop

Sometimes you want more than reporting. You want enforcement. One way is to make your test tool fail when coverage falls below a threshold.

Jest example

Jest supports coverage thresholds directly in configuration, such as package.json or jest.config.js.

module.exports = {
  coverageThreshold: {
    global: {
      lines: 80,
      branches: 70,
      functions: 80,
      statements: 80,
    },
  },
};

If the actual coverage falls below the threshold, the test job fails and the pipeline turns red.

Why this is better than hoping

A visible metric is helpful. An enforced quality gate is stronger. Coverage thresholds prevent silent erosion over time.

That said, teams should choose realistic thresholds. Arbitrary numbers can encourage meaningless tests written only to satisfy the metric.

Parallel Test Execution with parallel

As projects grow, tests get slower. One way to improve feedback speed is parallel execution.

GitLab supports the parallel keyword:

unit-tests:
  stage: test
  image: node:20
  parallel: 4
  script:
    - npm ci
    - npm test

This creates multiple job instances. To use it well, your test framework or wrapper script should know how to shard or distribute the tests across those parallel jobs.

Parallelization helps when:

  • your suite is large
  • runners have enough capacity
  • faster developer feedback is a priority

Keep in mind that parallel jobs may increase CI usage even while reducing wall-clock time.

Test Environment Variables in CI

Tests often need configuration values such as:

  • TEST_ENV
  • DATABASE_URL
  • API_BASE_URL
  • feature flag values
  • mock service credentials

You can define these in GitLab CI variables and use them safely in jobs.

integration-tests:
  stage: test
  image: node:20
  script:
    - npm ci
    - npm run test:integration
  variables:
    TEST_ENV: ci

For sensitive values, store them in Settings → CI/CD → Variables instead of hardcoding them in the YAML file.

This is especially important for integration tests that touch ephemeral databases, staging services, or external APIs.

Complete Example: Node.js App with Jest, Coverage, and JUnit Artifacts

Here is a full example of a practical GitLab CI testing pipeline for a Node.js project.

stages:
  - test

default:
  image: node:20

jest-tests:
  stage: test
  script:
    - npm ci
    - npm test -- --ci --coverage --reporters=default --reporters=jest-junit
  variables:
    JEST_JUNIT_OUTPUT_FILE: junit.xml
    TEST_ENV: ci
  coverage: '/All files[^|]*\|[^|]*\s+([0-9]+\.?[0-9]*)%/'
  artifacts:
    when: always
    paths:
      - coverage/
      - junit.xml
    reports:
      junit: junit.xml
      coverage_report:
        coverage_format: cobertura
        path: coverage/cobertura-coverage.xml
  rules:
    - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
    - if: '$CI_COMMIT_BRANCH == "main"'

What this pipeline gives you

  • Jest tests running in a clean Node.js container
  • coverage percentage extracted into GitLab
  • JUnit XML uploaded for structured test reporting
  • Cobertura coverage report uploaded for MR diff visualization
  • artifacts preserved even when the tests fail
  • execution on merge requests and on main branch pipelines

This is a great baseline for many JavaScript projects.

Example: Python with pytest and JUnit

For comparison, here is the same concept with Python.

pytest-tests:
  stage: test
  image: python:3.12
  script:
    - pip install -r requirements.txt
    - pip install pytest pytest-cov junitxml
    - pytest --junitxml=junit.xml --cov=. --cov-report=xml:coverage/cobertura-coverage.xml
  artifacts:
    when: always
    reports:
      junit: junit.xml
      coverage_report:
        coverage_format: cobertura
        path: coverage/cobertura-coverage.xml

The tools differ, but the GitLab pattern is the same: run tests, generate structured reports, upload them as artifacts, and let GitLab enrich the review experience.

Common Mistakes to Avoid

  1. Running tests without publishing reports. You lose visibility GitLab could have provided.
  2. Skipping when: always on test artifacts. Failed jobs often need reports most.
  3. Treating coverage as a perfect quality proxy. Coverage helps, but meaningful assertions matter more.
  4. Hardcoding secrets in YAML. Use CI/CD variables for anything sensitive.
  5. Never revisiting slow test suites. Parallelization and smarter test selection may be needed.

Final Takeaway

GitLab testing features do more than turn a job green or red. They let you build fast, visible, enforceable quality gates. Start with straightforward unit test jobs. Then add JUnit reports so merge requests show structured failures. Add coverage extraction and Cobertura visualization so reviewers can see what changed. Use thresholds when your team is ready for stronger enforcement. And if the suite gets slow, scale it with parallel execution and better CI design.


Knowledge Check

Exercise

Question 1: JUnit Reports

Why should you upload a JUnit XML report with artifacts:reports:junit in GitLab CI?

Exercise

Question 2: Coverage Regex

What is the purpose of the coverage keyword in a GitLab CI job?

Exercise

Question 3: Coverage Thresholds

What is a practical way to fail a pipeline when test coverage drops below an agreed threshold?

PreviousPrev
Next

Continue Learning

GitLab CI Pipeline Rules

Learn GitLab CI rules, rules if, rules changes, rules exists, workflow rules, merge request pipeline conditions, manual jobs, and how to control exactly when GitLab pipelines and jobs run.

12 min read·Easy

GitLab CI Environments

Learn GitLab CI environments, deployment tracking, review apps, environment URLs, protected environments, deployment approvals, scoped variables, DORA metrics, and a complete multi-environment deployment example.

12 min read·Medium

GitLab CI Docker Workflows

Learn GitLab CI Docker pipelines, per-job and global images, Docker-in-Docker, Kaniko, GitLab Container Registry authentication, Docker Hub pushes, tagging strategies, and a complete build and push example.

14 min read·Medium

Explore Related Topics

Do

Docker Tutorials

Build and push Docker images in your pipelines

Gi

Git Tutorials

Trigger CI/CD pipelines from Git events

Try the Tool

YAML Validator

Validate GitHub Actions and CI/CD pipeline YAML files.

Cron Parser

Build and understand scheduled workflow cron expressions.

On This Page

Why Automated Testing in CI Is Non-NegotiableRunning Unit Tests in a GitLab CI JobNode.js and Jest examplePython and pytest exampleCollecting JUnit XML ReportsWhy JUnit reports matterWhy `when: always` is helpfulViewing Test Results in the Merge Request UICode Coverage: What It MeasuresExtracting Coverage Percentage with `coverage` RegexWhy regex-based coverage existsCoverage Visualization in Merge Request DiffsCoverage Badges for README FilesFailing the Pipeline on Coverage DropJest exampleWhy this is better than hopingParallel Test Execution with `parallel`Test Environment Variables in CIComplete Example: Node.js App with Jest, Coverage, and JUnit ArtifactsWhat this pipeline gives youExample: Python with pytest and JUnitCommon Mistakes to AvoidFinal TakeawayKnowledge Check