CI Extension Lab: Mocks, Coverage, and Quality Gates
This lab extends the Python and Java pipelines you built in the earlier session. You will keep both repositories, add mocks and coverage, and make the pipelines enforce quality gates in GitHub Actions and Jenkins.
Starting point and what changes
Before you begin
You need the two repositories from the earlier session, both with passing pipelines:
| Repository | From the earlier session | Tools already installed |
|---|---|---|
python-ci-demo | app/calculator.py, app/main.py, tests/test_calculator.py, requirements.txt (pytest, pytest-cov, flake8), pytest.ini, .github/workflows/ci.yml, Jenkinsfile | Python 3.11+, Docker Jenkins |
java-ci-demo | pom.xml (compiler with -Werror, surefire, JaCoCo report, jar), Calculator.java, App.java, CalculatorTest.java, .github/workflows/ci.yml, Jenkinsfile | JDK 17+, Maven 3.9+, Docker Jenkins |
pytest in python-ci-demo and mvn -B verify in java-ci-demo. Both must pass. Fix any failure now, because this lab adds on top of the existing code.What this extension adds
| Area | Python | Java |
|---|---|---|
| New code under test | app/converter.py (depends on a rate provider) | OrderService.java (depends on an InventoryRepository) |
| Mocks | unittest.mock.Mock and patch | Mockito with @Mock and @InjectMocks |
| New dependency | None (pytest-cov is already pinned) | mockito-junit-jupiter |
| Coverage gate | --cov-fail-under=90 in the test command | JaCoCo check rules in pom.xml: line 80%, branch 70% |
| CI changes | Coverage step, job summary, and report upload in ci.yml; recordCoverage gate in the Jenkinsfile | Lint and verify steps, PR coverage comment, and recordCoverage gate in the Jenkinsfile |
Jenkins: install the Coverage plugin once
Both Jenkins jobs now use the recordCoverage step. Install the plugin one time for the whole Jenkins server:
- Open Jenkins at
http://localhost:8080, go to Manage Jenkins, then Plugins, then Available plugins. - Search for Coverage, select it, and click Install. Restart Jenkins if asked.
Coverage appears under Installed plugins.Part 1: Python extension python-ci-demo
Step 1.1 Activate the environment and confirm the baseline
Goal: start from a known, passing state.
Do
cd python-ci-demo
source .venv/bin/activate # Windows PowerShell: .venv\Scripts\Activate.ps1
git checkout main && git pull
pytest -v
Explanation
.venvis the virtual environment from the earlier session. Activating it makespythonandpytestrefer to the pinned versions.- Pulling first makes sure you extend the latest code, including any changes made in the earlier session.
3 passed.Step 1.2 Create the converter with an injected dependency
Goal: add code whose external rate service can be replaced by a mock in tests.
Do
from typing import Callable
RateProvider = Callable[[str, str], float]
class CurrencyConverter:
def __init__(self, rate_provider: RateProvider) -> None:
self._rate_provider = rate_provider
def convert(self, amount: float, src: str, dst: str) -> float:
if amount < 0:
raise ValueError("amount must be non-negative")
if src == dst:
return amount
rate = self._rate_provider(src, dst)
return round(amount * rate, 2)
Explanation
- The converter does not fetch rates itself. It receives a function in its constructor. In production that function calls a real service. In tests it is a mock.
- There are three paths: negative input is rejected, identical currencies return early without calling the provider, and everything else asks the provider. Each path needs a test.
Step 1.3 Write tests with mocks
Goal: test every path without any network access.
Do
from unittest.mock import Mock
import pytest
from app.converter import CurrencyConverter
def test_convert_uses_provider_rate():
provider = Mock(return_value=1.1)
converter = CurrencyConverter(provider)
assert converter.convert(10, "EUR", "USD") == 11.0
provider.assert_called_once_with("EUR", "USD")
def test_same_currency_skips_provider():
provider = Mock()
assert CurrencyConverter(provider).convert(5, "USD", "USD") == 5
provider.assert_not_called()
def test_negative_amount_rejected():
converter = CurrencyConverter(Mock())
with pytest.raises(ValueError):
converter.convert(-1, "EUR", "USD")
def test_provider_failure_propagates():
provider = Mock(side_effect=ConnectionError("rate service timeout"))
with pytest.raises(ConnectionError):
CurrencyConverter(provider).convert(1, "EUR", "USD")
Explanation
| Test | What it proves |
|---|---|
test_convert_uses_provider_rate | The rate is applied and rounded, and the provider is called once with the right currencies. assert_called_once_with checks the interaction, not just the result. |
test_same_currency_skips_provider | The early return works, and no network call is made. assert_not_called proves it. |
test_negative_amount_rejected | Invalid input raises before any provider call. |
test_provider_failure_propagates | A failing service does not get silently swallowed. side_effect makes the mock raise. |
Mock(return_value=...) returns a fixed value, Mock(side_effect=...) raises, assert_called_once_with and assert_not_called check how the mock was used.Step 1.4 Patch a function imported into a module
Goal: test run() without depending on the real add function.
Do
from unittest.mock import patch
import app.main
def test_run_prints_result_from_add(capsys):
with patch("app.main.add", return_value=99):
app.main.run()
assert "2 + 3 = 99" in capsys.readouterr().out
Explanation
patchreplaces a name only inside thewithblock, then restores it.- Patch where the name is used.
app.mainimportedadd, so the patch target isapp.main.add. Patchingapp.calculator.addwould not affectrun(). capsyscaptures what the function prints so the test can check it.
Step 1.5 Exclude the script guard from coverage
Goal: keep the coverage number about logic, not about the script wrapper.
Do
from app.calculator import add, divide
def run() -> None:
print(f"2 + 3 = {add(2, 3)}")
print(f"10 / 4 = {divide(10, 4)}")
if __name__ == "__main__": # pragma: no cover
run()
Explanation
- The guard runs only when the file is executed directly. Tests import the module, so this line can never be covered.
# pragma: no coveris a marker for coverage.py. Use it only for code that cannot run under test.
Step 1.6 Run the tests with the coverage gate
Goal: measure coverage and make the command fail below 90%.
Do
pytest -v --cov=app --cov-branch --cov-report=term-missing --cov-report=html:reports/htmlcov --cov-fail-under=90
echo "exit code: $?"
Explanation
| Flag | Meaning |
|---|---|
--cov=app | Measure only the application package. |
--cov-branch | Count the if outcomes as well as the lines. This catches an untested else that line coverage would miss. |
--cov-report=term-missing | Print the table and list uncovered lines. |
--cov-report=html:reports/htmlcov | Write a browsable HTML report for drill-down. |
--cov-fail-under=90 | The gate. Exit code 1 when total coverage is under 90%. |
8 passed, the TOTAL row shows 90% or more (with these tests it should be 100%), and exit code: 0.Step 1.7 Open the HTML report
open reports/htmlcov/index.html # Windows: start reports\htmlcov\index.html
Click calculator.py and converter.py. Lines are green when they ran. Yellow means a branch ran only one way. Red means the line never ran.
Step 1.8 Update the GitHub Actions workflow
Goal: enforce the same gate in CI and show coverage on the job page.
Do
name: Python CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.11", "3.12"]
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
cache: pip
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
- name: Lint
run: flake8 app tests
- name: Test with coverage gate
run: |
pytest --junitxml=reports/junit.xml \
--cov=app --cov-branch \
--cov-report=term-missing \
--cov-report=xml:reports/coverage.xml \
--cov-report=html:reports/htmlcov \
--cov-fail-under=90
- name: Coverage summary in job page
if: always()
run: |
{
echo "### Coverage (Python ${{ matrix.python-version }})"
echo '```'
python -m coverage report
echo '```'
} >> "$GITHUB_STEP_SUMMARY"
- name: Upload reports
if: always()
uses: actions/upload-artifact@v4
with:
name: python-reports-${{ matrix.python-version }}
path: reports/
build:
needs: test
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Package application
run: tar czf app-${{ github.sha }}.tar.gz app requirements.txt
- name: Upload package
uses: actions/upload-artifact@v4
with:
name: app-package
path: app-${{ github.sha }}.tar.gz
Explanation: what changed from the earlier workflow
| Change | Why |
|---|---|
Test step now runs with --cov-fail-under=90 | The step fails when coverage drops, which fails the job and blocks the build job. |
--junitxml and the coverage XML/HTML outputs | The JUnit file is for test results. The XML is the machine-readable coverage file. The HTML is for people. |
Coverage summary with if: always() | Writes the coverage table to the run page even when the gate fails, so you see the reason on the summary page. |
Report upload with if: always() | Keeps the HTML report for download on failed runs too. |
{ ... } >> "$GITHUB_STEP_SUMMARY" construct groups several echo lines and appends them to the file GitHub uses for job summaries. The backticks are written as '```' so the shell treats them as text.Step 1.9 Update the Jenkinsfile
Goal: run the same gate in Jenkins and record it with the Coverage plugin.
Do
pipeline {
agent any
options {
timestamps()
disableConcurrentBuilds()
buildDiscarder(logRotator(numToKeepStr: '10'))
}
stages {
stage('Checkout') {
steps {
checkout scm
}
}
stage('Lint and Test') {
steps {
sh '''
python3 -m venv .venv
. .venv/bin/activate
pip install -r requirements.txt
flake8 app tests
mkdir -p reports
pytest --junitxml=reports/junit.xml --cov=app --cov-branch --cov-report=term-missing --cov-report=xml:reports/coverage.xml --cov-report=html:reports/htmlcov --cov-fail-under=90
'''
}
post {
always {
junit testResults: 'reports/junit.xml', allowEmptyResults: true
recordCoverage(
tools: [[parser: 'COBERTURA', pattern: 'reports/coverage.xml']],
id: 'python',
name: 'Python coverage',
qualityGates: [[threshold: 90.0, metric: 'LINE', baseline: 'PROJECT', criticality: 'FAILURE']]
)
archiveArtifacts artifacts: 'reports/htmlcov/**', allowEmptyArchive: true
}
}
}
stage('Package') {
steps {
sh 'tar czf app-${BUILD_NUMBER}.tar.gz app requirements.txt'
archiveArtifacts artifacts: 'app-*.tar.gz', fingerprint: true
}
}
}
post {
always {
cleanWs()
}
}
}
Explanation
Lint and Testcombines the earlier Lint and Test stages into one stage, so the coverage gate and the lint step share the same virtual environment.junitinpostruns even when the tests fail, so the failure details are still visible on the build page.recordCoveragereads the Cobertura XML and applies a line-coverage threshold of 90%. Withcriticality: 'FAILURE', a value below 90% marks the build red.baseline: 'PROJECT'compares against the whole project. The Coverage plugin also provides a trend chart, so you can see whether coverage is rising or falling across builds.
Step 1.10 Commit, push, and observe both runs
Goal: see the Python pipelines run with the new gate.
Do
git checkout -b ext/python-coverage
git add . && git commit -m "Add converter with mocks, coverage gate, and CI updates"
git push -u origin ext/python-coverage
Open a pull request into main on GitHub. Then, in Jenkins, open the python-ci-demo job and click Build Now after merging, or wait for the next poll.
- GitHub: open the Actions run and read the Coverage summary at the bottom of the run page.
- Jenkins: open the build and then Python coverage in the left menu. Check the quality gate result and the per-file table.
main before continuing to Part 2.Part 2: Java extension java-ci-demo
Step 2.1 Confirm the baseline and create a branch
cd ../java-ci-demo
git checkout main && git pull
git checkout -b ext/java-mocks-coverage
mvn -B verify
BUILD SUCCESS with Tests run: 3.Step 2.2 Create the repository interface
Goal: define the dependency that the service will receive, so tests can replace it.
Do
package com.example.calc;
public interface InventoryRepository {
int stockOf(String sku);
}
Explanation
- An interface states what the dependency does without saying how. A database class, an in-memory class, and a Mockito mock can all implement or stand in for it.
- The same idea as the Python
RateProvidertype alias. The dependency is a named contract.
Step 2.3 Create the service with constructor injection
Goal: write logic that depends only on the interface.
Do
package com.example.calc;
public final class OrderService {
private final InventoryRepository inventory;
public OrderService(InventoryRepository inventory) {
this.inventory = inventory;
}
public boolean canFulfil(String sku, int quantity) {
if (quantity <= 0) {
throw new IllegalArgumentException("quantity must be positive");
}
return inventory.stockOf(sku) >= quantity;
}
}
Explanation
- The repository arrives through the constructor. The service never creates one, which is what makes the mock possible.
- Validation runs before the repository is touched. A test can then prove that invalid input never calls the repository.
Step 2.4 Write Mockito tests
Goal: test each path, including the interaction checks.
Do
package com.example.calc;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
@ExtendWith(MockitoExtension.class)
class OrderServiceTest {
@Mock
private InventoryRepository inventory;
@InjectMocks
private OrderService service;
@Test
void canFulfilWhenStockIsSufficient() {
when(inventory.stockOf("ABC")).thenReturn(10);
assertTrue(service.canFulfil("ABC", 5));
verify(inventory).stockOf("ABC");
}
@Test
void cannotFulfilWhenStockIsShort() {
when(inventory.stockOf("ABC")).thenReturn(2);
assertFalse(service.canFulfil("ABC", 5));
}
@Test
void rejectsNonPositiveQuantityWithoutTouchingInventory() {
assertThrows(IllegalArgumentException.class, () -> service.canFulfil("ABC", 0));
verifyNoInteractions(inventory);
}
}
Explanation
| Annotation or call | Meaning |
|---|---|
@ExtendWith(MockitoExtension.class) | Connects Mockito to JUnit and creates the mocks before each test. |
@Mock | A fake InventoryRepository. Unstubbed methods return 0, null, or false. |
@InjectMocks | Builds the OrderService and passes the mock through its constructor. |
when(...).thenReturn(...) | Stubbing: sets the return value for a call. |
verify(inventory).stockOf("ABC") | Checks the call happened with that argument. |
verifyNoInteractions(inventory) | Checks the mock was never used. |
UnnecessaryStubbingException. This keeps tests honest. You will see it in Part 4.Step 2.5 Add Mockito and the coverage rule to pom.xml
Goal: make Mockito available and make the build fail when coverage is too low.
Do
(a) Inside <dependencies>, add after the JUnit dependency:
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<version>5.11.0</version>
<scope>test</scope>
</dependency>
(b) Inside the jacoco-maven-plugin element, add a plugin-level <configuration> before <executions>:
<configuration>
<excludes>
<exclude>com/example/calc/App.class</exclude>
</excludes>
</configuration>
(c) Inside the same plugin's <executions>, add a check execution after the report execution:
<execution>
<id>check</id>
<goals><goal>check</goal></goals>
<configuration>
<rules>
<rule>
<element>BUNDLE</element>
<limits>
<limit>
<counter>LINE</counter>
<value>COVEREDRATIO</value>
<minimum>0.80</minimum>
</limit>
<limit>
<counter>BRANCH</counter>
<value>COVEREDRATIO</value>
<minimum>0.70</minimum>
</limit>
</limits>
</rule>
</rules>
</configuration>
</execution>
Explanation
- (a)
mockito-junit-jupiterbrings in Mockito and the JUnit 5 extension. Thetestscope keeps it out of the jar. - (b)
excludesremovesApp.classfrom coverage. The entry point only prints, and the jar smoke test in Step 2.7 covers it instead. - (c) The
checkgoal runs duringverify.BUNDLEapplies the rule to the whole project.COVEREDRATIOwithminimummeans the covered fraction must be at least that value. Line must be at least 80% and branch at least 70%.
Step 2.6 Run the build with the gate
mvn -B verify
echo "exit code: $?"
Tests run: 6, Failures: 0, Errors: 0, a JaCoCo line confirming that coverage checks were met, BUILD SUCCESS, and exit code: 0.Step 2.7 Smoke-test the jar and read the JaCoCo report
java -jar target/calc-ci-demo-1.0.0.jar
open target/site/jacoco/index.html # Windows: start target\site\jacoco\index.html
Explanation
- The jar run prints
2 + 3 = 5and10 / 4 = 2.5. That coversApp.main, which the coverage rule excludes. - In the JaCoCo report, click
OrderServiceto see which lines and branches ran. Every line should be green.
Step 2.8 Update the GitHub Actions workflow
Goal: run lint and verify in CI, comment coverage on pull requests, and keep the jar build.
Do
name: Java CI
on:
push:
branches: [main]
pull_request:
branches: [main]
permissions:
contents: read
pull-requests: write
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
java-version: ["17", "21"]
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up JDK ${{ matrix.java-version }}
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: ${{ matrix.java-version }}
cache: maven
- name: Lint (compile with warnings as errors)
run: mvn -B compile
- name: Test and JaCoCo gate
run: mvn -B verify
- name: Coverage comment on pull request
if: github.event_name == 'pull_request' && always()
uses: madrapps/jacoco-report@v1.6.1
with:
paths: ${{ github.workspace }}/target/site/jacoco/jacoco.xml
token: ${{ secrets.GITHUB_TOKEN }}
min-coverage-overall: 80
min-coverage-changed-files: 80
title: Java coverage
- name: Upload reports
if: always()
uses: actions/upload-artifact@v4
with:
name: java-reports-${{ matrix.java-version }}
path: |
target/surefire-reports/
target/site/jacoco/
build:
needs: test
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: "17"
cache: maven
- name: Package application
run: mvn -B -DskipTests package
- name: Upload jar
uses: actions/upload-artifact@v4
with:
name: calc-jar
path: target/*.jar
Explanation: what changed
| Change | Why |
|---|---|
Separate mvn -B compile step | Lint runs first and fails fast. Any compiler warning stops the job before the slower tests run. |
mvn -B verify step | Runs the tests and applies the JaCoCo rule. A coverage failure fails the job. |
Top-level permissions | pull-requests: write lets the coverage comment step post on pull requests. |
| Coverage comment step | Posts overall and changed-file coverage on the pull request. Fails the check when either is below 80%. |
if: github.event_name == 'pull_request' && always() | Limits the comment to pull requests and keeps it running after a failed test step. |
Upload reports with if: always() | Keeps the surefire and JaCoCo reports for download, even when the build fails. |
Step 2.9 Update the Jenkinsfile
Goal: record JaCoCo coverage in Jenkins with its own gate.
Do
pipeline {
agent any
options {
timestamps()
disableConcurrentBuilds()
buildDiscarder(logRotator(numToKeepStr: '10'))
}
stages {
stage('Checkout') {
steps {
checkout scm
}
}
stage('Lint') {
steps {
sh 'mvn -B compile'
}
}
stage('Test') {
steps {
sh 'mvn -B verify'
}
post {
always {
junit testResults: 'target/surefire-reports/*.xml', allowEmptyResults: true
recordCoverage(
tools: [[parser: 'JACOCO', pattern: 'target/site/jacoco/jacoco.xml']],
id: 'java',
name: 'Java coverage',
qualityGates: [[threshold: 80.0, metric: 'LINE', baseline: 'PROJECT', criticality: 'FAILURE']]
)
archiveArtifacts artifacts: 'target/site/jacoco/**', allowEmptyArchive: true
}
}
}
stage('Package') {
steps {
sh 'mvn -B -DskipTests package'
archiveArtifacts artifacts: 'target/*.jar', fingerprint: true
}
}
}
post {
always {
cleanWs()
}
}
}
Explanation
- Lint and Test are separate stages so the Jenkins stage view shows which one failed.
recordCoveragewithJACOCOreads the samejacoco.xmlthat the Maven rule reads. Maven fails first when the rule is broken, but the Jenkins gate is a second, visible check with a trend chart.junitreads surefire XML, so the Test Result page lists each test method.
Step 2.10 Commit, push, and observe both runs
git add . && git commit -m "Add OrderService with Mockito tests, JaCoCo gate, and CI updates"
git push -u origin ext/java-mocks-coverage
Open a pull request into main. Check the GitHub run and the coverage comment, then build in Jenkins with Build Now.
Part 3: Reading the coverage reports
Step 3.1 Read the Python terminal table
The term-missing report looks like this:
Name Stmts Miss Branch BrPart Cover Missing
----------------------------------------------------------------
app/calculator.py 7 0 4 0 100%
app/converter.py 10 0 6 0 100%
app/main.py 4 0 0 0 100%
----------------------------------------------------------------
TOTAL 21 0 10 0 100%
How to read it
- Stmts is the number of statements. Miss is how many did not run.
- Branch counts decision outcomes. BrPart counts decisions where only one outcome ran.
- Missing lists the uncovered line numbers.
12means line 12 never ran.19->21means the jump from line 19 to 21 never happened, which is usually an untestedifoutcome. - TOTAL combines statements and branches, which is the number
--cov-fail-underchecks.
Step 3.2 Read the JaCoCo summary
Open target/site/jacoco/index.html. The top table shows Missed and Covered for Instructions, Branches, Lines, Methods, and Classes. The Maven rule uses the Lines and Branches rows. In the class view, green is fully covered, yellow means some branches were missed (a diamond shows how many), and red means the code never ran.
Step 3.3 Connect the report to the pipeline
| Question | Where to look |
|---|---|
| Why did the Python build fail? | The Python job's Test with coverage gate step in GitHub, or the Lint and Test stage in Jenkins, then the FAIL Required test coverage line |
| Which Java line is missing a test? | The yellow or red line in target/site/jacoco/index.html, or the PR coverage comment |
| Is coverage going down over time? | The trend chart on the Jenkins Python coverage or Java coverage page |
| Which tests ran? | The Jenkins Test Result page, or the reports/junit.xml and target/surefire-reports/ files |
Part 4: Breaking the quality gates
git checkout -b lab/break-gates
Exercise 4.1: Python coverage gate
Goal: see the coverage gate stop the Python pipeline.
- Change: delete
test_negative_amount_rejectedfromtests/test_converter.py. - Run:
pytest --cov=app --cov-branch --cov-report=term-missing --cov-fail-under=90
echo "exit code: $?"
- Expected:
converter.pylists theraiseline under Missing, and the last line readsFAIL Required test coverage of 90% not reached. The exit code is 1. - Why: the branch
if amount < 0never took its true path, so the coverage total dropped below the gate. - Push and check that the GitHub test job turns red and the Jenkins Lint and Test stage fails. Then restore the test and push again.
Exercise 4.2: Java JaCoCo rule
Goal: see the Maven rule stop the Java build.
- Change: delete
rejectsNonPositiveQuantityWithoutTouchingInventoryfromOrderServiceTest.java. - Run:
mvn -B verify - Expected: a warning similar to Rule violated for bundle calc-ci-demo: branches covered ratio is 0.50, but expected minimum is 0.70, then
BUILD FAILURE. - Find the cause: open
target/site/jacoco/index.html, clickOrderService, and look for the yellow line onif (quantity <= 0). - Restore the test and confirm
BUILD SUCCESS.
Exercise 4.3: Mockito strict stubs
Goal: understand why an unused stub is a failure.
- Change: in
cannotFulfilWhenStockIsShort, delete theassertFalse(...)line and keep thewhen(...)line. - Run:
mvn -B test - Expected:
UnnecessaryStubbingException, even though no assertion fails. - Why: the test no longer checks its result, so it would pass without testing anything. Restore the assertion.
Exercise 4.4: Java lint gate
Goal: see -Werror fail the build on a warning.
- Change: add this line inside
maininApp.java:
java.util.List raw = new java.util.ArrayList();
- Run:
mvn -B compile - Expected: a
rawtypeswarning, thenwarnings found and -Werror specified. The tests never run. - Restore by deleting the line.
git checkout main && git branch -D lab/break-gates.Part 5: Wrap-up and troubleshooting
Final checklist
| Skill | Done |
|---|---|
| I can inject a dependency and replace it with a mock in Python and in Java | ☐ |
| I can explain line coverage and branch coverage using a real example from my code | ☐ |
| I can run the coverage gate locally and read the Missing column | ☐ |
I know which file sets each gate: pytest command, pom.xml, and the Jenkinsfile | ☐ |
| I can read the GitHub job summary and the PR coverage comment | ☐ |
| I can read the Jenkins Coverage pages and their quality gate results | ☐ |
| I have broken and fixed each gate at least once | ☐ |
Troubleshooting
| Symptom | Cause and fix |
|---|---|
ModuleNotFoundError: app in pytest | Check that pytest.ini has pythonpath = . and run from the repository root. |
| pytest shows 0% or "No data was collected" | Check --cov=app matches the package folder and that app/__init__.py exists. |
Jenkins: No such DSL method 'recordCoverage' | The Coverage plugin is not installed, or Jenkins was not restarted after installing it. See the plugin step at the start of this lab. |
| Jenkins coverage page is empty | The pattern does not match the file path. For Python it must be reports/coverage.xml. For Java it must be target/site/jacoco/jacoco.xml. |
invalid target release: 17 | The local JDK is older than 17. Install JDK 17 or later and set JAVA_HOME. |
Rule violated for bundle | Coverage is below the minimum. Open the JaCoCo report and find the missed lines and branches. |
UnnecessaryStubbingException | A stub is never used. Remove it or add the assertion that uses it. |
| PR coverage comment missing | The workflow needs the top-level permissions block with pull-requests: write. Pull requests from forks get a read-only token. |
Jenkins: python3: not found or mvn: not found | The tools were not installed in the Jenkins container. Re-run the install command from the earlier session. |
Next steps
- Raise each threshold by one or two points as you add tests. This is called ratcheting.
- Add a mutation testing tool (
mutmutfor Python, PIT for Java) to check that tests fail when code is changed. - Protect
mainin GitHub: require the Python and Java checks to pass before a merge. - Move coverage gating to changed lines only, so legacy code does not block new work.