Skip to content

Tutorial: Python Django App

This tutorial walks through adding nolapse coverage enforcement to a Django application. It covers pytest-cov configuration, baseline creation, regression simulation, and CI integration.

Time to complete: 20–30 minutes.


  • Python 3.9 or later
  • nolapse CLI installed (go install github.com/nolapse/nolapse-cli/nolapse-cli/cmd/nolapse@latest)

Terminal window
mkdir hello-django && cd hello-django
git init
python3 -m venv .venv
source .venv/bin/activate
pip install django pytest pytest-cov pytest-django
django-admin startproject config .
python manage.py startapp greet

Edit greet/views.py:

from django.http import JsonResponse
def health(request):
return JsonResponse({"status": "ok"})
def greet(request):
name = request.GET.get("name", "world")
return JsonResponse({"message": f"Hello, {name}!"})

Edit config/urls.py:

from django.urls import path
from greet import views
urlpatterns = [
path("health/", views.health),
path("greet/", views.greet),
]

Create pytest.ini:

[pytest]
DJANGO_SETTINGS_MODULE = config.settings
addopts = --cov=greet --cov-report=json --cov-report=term-missing

Create greet/tests.py:

import pytest
from django.test import Client
@pytest.fixture
def client():
return Client()
def test_health(client):
response = client.get("/health/")
assert response.status_code == 200
assert response.json() == {"status": "ok"}
def test_greet_default(client):
response = client.get("/greet/")
assert response.status_code == 200
assert response.json() == {"message": "Hello, world!"}
def test_greet_named(client):
response = client.get("/greet/?name=Alice")
assert response.status_code == 200
assert response.json() == {"message": "Hello, Alice!"}
Terminal window
pytest

Expected output (last lines):

---------- coverage: platform linux, python 3.12 ----------
Name Stmts Miss Cover
-------------------------------------
greet/views.py 8 0 100%
-------------------------------------
TOTAL 8 0 100%
3 passed in 0.42s

Terminal window
nolapse init --repo . --lang python

Expected output:

measuring coverage...
coverage: 100.00%
baseline written to .audit/coverage/baseline.md
nolapse.yaml created
Terminal window
cat .audit/coverage/baseline.md
coverage: 100.00%
timestamp: 2026-03-18T09:00:00Z
commit: a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2
Terminal window
cat nolapse.yaml
lang: python
warn_threshold: 0.5
fail_threshold: 1.0
Terminal window
git add .audit/coverage/baseline.md nolapse.yaml greet/ config/ pytest.ini manage.py
git commit -m "chore: add nolapse coverage baseline"
Terminal window
nolapse run --repo . --lang python

Expected output:

file baseline coverage PR coverage delta outcome
. 100.00% 100.00% +0.00% pass
outcome: pass delta: +0.00 coverage: 100.00% baseline: 100.00%

Delete test_greet_named from greet/tests.py. Save the file.

Terminal window
nolapse run --repo . --lang python

Expected output:

file baseline coverage PR coverage delta outcome
. 100.00% 87.50% -12.50% fail
outcome: fail delta: -12.50 coverage: 87.50% baseline: 100.00%
warn_threshold: 0.5 fail_threshold: 1.0

Exit code is 1. The regression was detected.

Add test_greet_named back. Re-run:

Terminal window
nolapse run --repo . --lang python
file baseline coverage PR coverage delta outcome
. 100.00% 100.00% +0.00% pass

Create .github/workflows/coverage.yml:

name: Coverage check
on:
pull_request:
jobs:
coverage:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install dependencies
run: pip install django pytest pytest-cov pytest-django
- name: Run nolapse
uses: nolapse/nolapse-action@v1
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
lang: python
warn-threshold: "0.5"
fail-threshold: "1.0"

Commit and push:

Terminal window
git add .github/workflows/coverage.yml
git commit -m "ci: add nolapse coverage gate"
git push -u origin main

Open a pull request. The coverage check runs automatically on every PR.


  • A Django app with views tested via pytest-django
  • pytest.ini configured to generate coverage.json on every run
  • A nolapse baseline committed to git
  • A GitHub Actions workflow enforcing coverage on every PR