To check test coverage in a Django project, measure it locally with coverage.py (coverage run --source='.' manage.py test), then publish and track the percentage over time with Coveralls, a hosted service that comments on every pull request and renders a README badge. coverage.py records which lines - and, with branch coverage, which decision paths - your tests actually execute; Coveralls turns that raw data into history, per-file drill-downs, and a pass/fail status check in CI.
Key takeaways
- coverage.py is the standard tool:
pip install coverage, run the suite through it, thencoverage report -morcoverage html. - Configure it once in
.coveragercor[tool.coverage.*]inpyproject.toml- setsource,omitmigrations/tests/venv, and turn onbranch = True. - For pytest-django users, pytest-cov wraps the same engine:
pytest --cov=myapp --cov-report=xml. - Coveralls publishes the result from CI (GitHub Actions, GitLab CI, etc.), tracks the trend, and is free for public/open-source repos (paid for private).
- Generate
coverage.xml/LCOV in CI, upload it with thecoverallsapp/github-actionor thecoverallsPyPI package, then add the badge to your README.
How do you measure Django test coverage with coverage.py?
coverage.py is the de-facto coverage tool for Python and works with Django's built-in test runner (which wraps unittest). Install it, then run your suite through coverage run instead of calling manage.py test directly.
# Install (inside your virtualenv)
pip install coverage
# Run the Django test suite under coverage, measuring the whole project
coverage run --source='.' manage.py test
# Print a terminal report; -m lists the exact missing line numbers
coverage report -m
# Build a browsable HTML report in ./htmlcov/ (open htmlcov/index.html)
coverage html--source='.' tells coverage.py to measure your project code and ignore the standard library and site-packages. coverage report -m prints a per-file table with the percentage covered and the Missing line numbers, so you can jump straight to untested code. To scope a run to specific apps, pass --source=app1,app2. A typical terminal report looks like this:
Name Stmts Miss Cover Missing
--------------------------------------------------------
myapp/__init__.py 0 0 100%
myapp/models.py 42 3 93% 55-57
myapp/views.py 88 14 84% 30-31, 90-104
--------------------------------------------------------
TOTAL 130 17 87%What is branch coverage, and should you enable it?
Statement (line) coverage only proves a line ran. Branch coverage additionally checks that each decision - both sides of every if/else, loop entry and exit, and try/except path - was exercised. A line such as if user.is_staff: can show 100% line coverage while the False branch is never tested. Turn it on with branch = True in your config (recommended) or --branch on the CLI.
coverage run --branch --source='.' manage.py testHow do you configure coverage.py with .coveragerc or pyproject.toml?
Rather than repeating flags, keep settings in a config file at your repo root. A .coveragerc excludes migrations, tests, settings, and the virtualenv so the reported percentage reflects real application logic.
# .coveragerc
[run]
source = .
branch = True
omit =
*/migrations/*
*/tests/*
*/venv/*
*/.venv/*
manage.py
*/settings/*
*/wsgi.py
*/asgi.py
[report]
show_missing = True
skip_covered = True
# Fail the run (exit non-zero) if total coverage drops below this percentage
fail_under = 85
exclude_lines =
pragma: no cover
raise NotImplementedError
if __name__ == .__main__.:If you already use pyproject.toml, put the same options under [tool.coverage.*] instead of a separate file:
# pyproject.toml
[tool.coverage.run]
source = ["."]
branch = true
omit = ["*/migrations/*", "*/tests/*", "*/venv/*", "manage.py"]
[tool.coverage.report]
show_missing = true
skip_covered = true
fail_under = 85coverage.py or pytest-cov - which should you use?
If you run Django tests with manage.py test, use coverage.py directly. If you have adopted pytest-django, the pytest-cov plugin wraps the same coverage.py engine and integrates with pytest options and parallel runs (pytest-xdist).
pip install pytest pytest-django pytest-cov
# Measure the myapp package; emit terminal + Cobertura XML for CI upload
pytest --cov=myapp --cov-report=term-missing --cov-report=xmlBoth paths drive the same engine and produce the same coverage.xml/LCOV that report hosts consume, so the choice is about your test runner, not the numbers.
| Aspect | coverage.py (direct) | pytest-cov plugin |
|---|---|---|
| Test runner | Django / unittest (manage.py test) |
pytest / pytest-django |
| Invocation | coverage run then coverage report |
single pytest --cov command |
| Underlying engine | coverage.py | coverage.py (same library) |
| Config file | .coveragerc / pyproject.toml |
same, plus pytest.ini / pyproject.toml |
| Parallel tests | --parallel + coverage combine |
pytest-xdist, auto-combined |
| Best fit | stock Django suites | teams already on pytest |
How do you publish coverage to Coveralls from CI?
Coveralls is a hosted service that ingests a coverage report on every push or pull request, stores the history, shows per-file drill-downs, and posts a status check so a PR fails if coverage drops. It is free for public/open-source repositories and offers paid plans for private repos - link your repo at coveralls.io through your Git host. Coverage pays off most when it runs automatically in CI; see our guide on automating Django tests in GitLab CI with Docker for the pipeline side.
A minimal GitHub Actions workflow that runs the Django suite under coverage and uploads to Coveralls:
# .github/workflows/ci.yml
name: CI
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.12'
- run: pip install -r requirements.txt coverage
- name: Run tests with coverage
run: |
coverage run --source='.' manage.py test
coverage lcov # writes coverage.lcov for Coveralls
- name: Upload to Coveralls
uses: coverallsapp/github-action@v2
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
file: coverage.lcovPrefer the Python client? Install coveralls and run it after the test step - it reads the .coverage data file and auto-detects the CI environment:
pip install coveralls
coverage run --source='.' manage.py test
COVERALLS_REPO_TOKEN=$COVERALLS_REPO_TOKEN coverallsGitLab CI works the same way: generate coverage.xml with coverage xml (or pytest-cov) and upload it with the coveralls package in a job script.
How do you add a coverage badge to your README?
Once a repo reports to Coveralls, copy the Markdown badge from your project's Coveralls page into README.md so the live percentage shows on your repo home page and in pull requests.
[](https://coveralls.io/github/your-org/your-repo?branch=main)Coveralls vs Codecov: which report host?
Coveralls and Codecov both host coverage reports, comment on pull requests, and render badges; they consume the same coverage.xml/LCOV files, so switching between them is low-cost. Choose on workflow fit, not on the coverage numbers themselves.
| Aspect | Coveralls | Codecov |
|---|---|---|
| Input formats | LCOV, Cobertura XML | Cobertura XML, LCOV, and more |
| PR comment / status | Yes | Yes (configurable via codecov.yml) |
| Public / open-source repos | Free | Free |
| Private repos | Paid | Paid |
| Coverage diff on PR | Yes | Yes (component/flag breakdown) |
| CI uploader | coverallsapp/github-action / coveralls pkg |
codecov/codecov-action |
For broader suite quality - readable fixtures and test data - see factory_boy: a better alternative to fixtures. If you would rather hand this off, our software testing services team sets up coverage gates and CI for Django projects.
Frequently Asked Questions
How do I check test coverage in a Django project?
Install coverage.py (pip install coverage), run your suite through it with coverage run --source='.' manage.py test, then use coverage report -m for a terminal summary or coverage html for a browsable report in htmlcov/. Add a .coveragerc to exclude migrations and tests so the percentage reflects real application code.
What is a good test coverage percentage for Django?
There is no universal number, but many teams gate at 80-90% and enforce it with fail_under in config. Treat coverage as a floor that surfaces untested code, not a goal in itself - 100% line coverage can still hide untested branches and weak assertions, which is why branch coverage matters.
Is Coveralls free?
Coveralls is free for public/open-source repositories and offers paid plans for private repos. You connect a repository through your Git host (GitHub, GitLab, or Bitbucket) and upload a coverage report from CI on each push or pull request.
What is the difference between line coverage and branch coverage?
Line (statement) coverage records whether each line ran; branch coverage also checks that every decision path - both sides of each if, loop, and try/except - executed. Enable branch coverage with branch = True in config or --branch on the command line for a stricter, more honest measure.
Should I use coverage.py or pytest-cov?
Use coverage.py directly with Django's manage.py test. If your suite runs on pytest-django, pytest-cov wraps the same engine and lets you measure coverage in a single pytest --cov=myapp --cov-report=xml command. Both emit the same report files that Coveralls and Codecov consume.
How do I add a coverage badge to my README?
After your repo reports to Coveralls, copy the Markdown badge snippet from your Coveralls project page into README.md. It renders the current percentage and links back to the full report, so contributors see coverage on the repo home page and inside pull requests.