CI Integration Guide¶
This guide provides complete, copy-paste ready CI configuration examples for using pytest-test-categories in various CI/CD platforms.
Core Principles¶
When integrating pytest-test-categories with CI:
Run small tests first - Get fast feedback (< 2 minutes)
Run tests in parallel - Small and medium tests can run concurrently
Gate large tests - Only run on main branch or before release
Generate reports - Track test distribution over time
Enforce strictly - Use
--test-categories-enforcement=strict
GitHub Actions¶
Basic Workflow¶
A minimal workflow that runs all tests with size enforcement:
# .github/workflows/test.yml
name: Tests
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install dependencies
run: |
pip install uv
uv sync --all-groups
- name: Run tests with enforcement
run: |
uv run pytest \
--test-categories-enforcement=strict \
--test-size-report=detailed \
-v
Complete Multi-Stage Workflow¶
A production-ready workflow with parallel test execution:
# .github/workflows/test.yml
name: Test Suite
on:
push:
branches: [main]
pull_request:
branches: [main]
env:
UV_CACHE_DIR: /tmp/.uv-cache
jobs:
# ===========================================================================
# Small Tests: Fast feedback (runs first, < 2 minutes)
# ===========================================================================
small-tests:
name: Small Tests (Python ${{ matrix.python-version }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: ["3.11", "3.12", "3.13"]
steps:
- uses: actions/checkout@v4
- name: Install uv
uses: astral-sh/setup-uv@v4
with:
enable-cache: true
cache-dependency-glob: "uv.lock"
- name: Set up Python ${{ matrix.python-version }}
run: uv python install ${{ matrix.python-version }}
- name: Install dependencies
run: uv sync --all-groups
- name: Run small tests
run: |
uv run pytest -m small \
--test-categories-enforcement=strict \
--test-size-report=json \
--test-size-report-file=small-tests-report.json \
-v
- name: Upload test report
uses: actions/upload-artifact@v4
if: always()
with:
name: small-tests-report-py${{ matrix.python-version }}
path: small-tests-report.json
# ===========================================================================
# Medium Tests: Container-based tests (runs in parallel with small)
# ===========================================================================
medium-tests:
name: Medium Tests
runs-on: ubuntu-latest
# No needs: - runs in parallel with small-tests
services:
postgres:
image: postgres:15-alpine
env:
POSTGRES_USER: test
POSTGRES_PASSWORD: test
POSTGRES_DB: test
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v4
- name: Install uv
uses: astral-sh/setup-uv@v4
with:
enable-cache: true
cache-dependency-glob: "uv.lock"
- name: Set up Python
run: uv python install 3.12
- name: Install dependencies
run: uv sync --all-groups
- name: Run medium tests
env:
DATABASE_URL: postgresql://test:test@localhost:5432/test
run: |
uv run pytest -m medium \
--test-categories-enforcement=strict \
--test-size-report=json \
--test-size-report-file=medium-tests-report.json \
-v
- name: Upload test report
uses: actions/upload-artifact@v4
if: always()
with:
name: medium-tests-report
path: medium-tests-report.json
# ===========================================================================
# Large Tests: Full integration (only on main branch)
# ===========================================================================
large-tests:
name: Large Tests
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
needs: [small-tests, medium-tests]
steps:
- uses: actions/checkout@v4
- name: Install uv
uses: astral-sh/setup-uv@v4
with:
enable-cache: true
cache-dependency-glob: "uv.lock"
- name: Set up Python
run: uv python install 3.12
- name: Install dependencies
run: uv sync --all-groups
- name: Run large tests
run: |
uv run pytest -m large \
--test-categories-enforcement=strict \
--test-size-report=json \
--test-size-report-file=large-tests-report.json \
-v
- name: Upload test report
uses: actions/upload-artifact@v4
if: always()
with:
name: large-tests-report
path: large-tests-report.json
# ===========================================================================
# Coverage: Combined coverage from all test sizes
# ===========================================================================
coverage:
name: Coverage Report
runs-on: ubuntu-latest
needs: [small-tests, medium-tests]
steps:
- uses: actions/checkout@v4
- name: Install uv
uses: astral-sh/setup-uv@v4
with:
enable-cache: true
cache-dependency-glob: "uv.lock"
- name: Set up Python
run: uv python install 3.12
- name: Install dependencies
run: uv sync --all-groups
- name: Run all tests with coverage
run: |
uv run coverage run -m pytest \
--test-categories-enforcement=warn \
--test-size-report=detailed
uv run coverage report --show-missing
uv run coverage xml
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v4
with:
files: coverage.xml
fail_ci_if_error: false
# ===========================================================================
# Distribution Check: Validate 80/15/5 distribution
# ===========================================================================
distribution-check:
name: Check Test Distribution
runs-on: ubuntu-latest
needs: [small-tests]
steps:
- uses: actions/checkout@v4
- name: Install uv
uses: astral-sh/setup-uv@v4
with:
enable-cache: true
cache-dependency-glob: "uv.lock"
- name: Set up Python
run: uv python install 3.12
- name: Install dependencies
run: uv sync --all-groups
- name: Check distribution
run: |
uv run pytest --collect-only \
--test-categories-distribution-enforcement=warn \
-q
PR-Only Small Tests¶
Skip slow tests on pull requests:
# .github/workflows/pr-tests.yml
name: PR Tests
on:
pull_request:
branches: [main]
jobs:
quick-tests:
name: Quick Tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install dependencies
run: pip install -e .[test]
- name: Run small tests only
run: |
pytest -m small \
--test-categories-enforcement=strict \
-v
GitLab CI¶
Basic Configuration¶
# .gitlab-ci.yml
stages:
- test
- coverage
variables:
PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip"
cache:
paths:
- .cache/pip/
# Small tests - run first and fast
small-tests:
stage: test
image: python:3.12-slim
script:
- pip install uv
- uv sync --all-groups
- uv run pytest -m small --test-categories-enforcement=strict -v
artifacts:
reports:
junit: junit.xml
paths:
- small-tests-report.json
when: always
# Medium tests - run in parallel with small
medium-tests:
stage: test
image: python:3.12-slim
services:
- name: postgres:15-alpine
alias: postgres
variables:
POSTGRES_USER: test
POSTGRES_PASSWORD: test
POSTGRES_DB: test
DATABASE_URL: postgresql://test:test@postgres:5432/test
script:
- pip install uv
- uv sync --all-groups
- uv run pytest -m medium --test-categories-enforcement=strict -v
artifacts:
reports:
junit: junit.xml
paths:
- medium-tests-report.json
when: always
# Large tests - only on main branch
large-tests:
stage: test
image: python:3.12-slim
rules:
- if: $CI_COMMIT_BRANCH == "main"
script:
- pip install uv
- uv sync --all-groups
- uv run pytest -m large --test-categories-enforcement=strict -v
artifacts:
reports:
junit: junit.xml
when: always
# Coverage report
coverage:
stage: coverage
image: python:3.12-slim
needs: [small-tests, medium-tests]
script:
- pip install uv
- uv sync --all-groups
- uv run coverage run -m pytest --test-categories-enforcement=warn
- uv run coverage report
- uv run coverage xml
coverage: '/TOTAL.*\s+(\d+%)$/'
artifacts:
reports:
coverage_report:
coverage_format: cobertura
path: coverage.xml
Complete GitLab Configuration¶
# .gitlab-ci.yml
stages:
- lint
- test
- coverage
- deploy
variables:
PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip"
cache:
key: "$CI_JOB_NAME"
paths:
- .cache/pip/
- .venv/
.python-base:
image: python:3.12-slim
before_script:
- pip install uv
- uv sync --all-groups
# ===========================================================================
# Linting Stage
# ===========================================================================
lint:
extends: .python-base
stage: lint
script:
- uv run ruff check .
- uv run ruff format --check .
- uv run mypy src/
# ===========================================================================
# Test Stage
# ===========================================================================
small-tests:
extends: .python-base
stage: test
parallel:
matrix:
- PYTHON_VERSION: ["3.11", "3.12", "3.13"]
image: python:${PYTHON_VERSION}-slim
script:
- uv run pytest -m small \
--test-categories-enforcement=strict \
--test-size-report=json \
--test-size-report-file=small-tests-report.json \
--junitxml=junit-small.xml \
-v
artifacts:
reports:
junit: junit-small.xml
paths:
- small-tests-report.json
when: always
medium-tests:
extends: .python-base
stage: test
services:
- name: postgres:15-alpine
alias: postgres
- name: redis:7-alpine
alias: redis
variables:
POSTGRES_USER: test
POSTGRES_PASSWORD: test
POSTGRES_DB: test
DATABASE_URL: postgresql://test:test@postgres:5432/test
REDIS_URL: redis://redis:6379
script:
- uv run pytest -m medium \
--test-categories-enforcement=strict \
--test-size-report=json \
--test-size-report-file=medium-tests-report.json \
--junitxml=junit-medium.xml \
-v
artifacts:
reports:
junit: junit-medium.xml
paths:
- medium-tests-report.json
when: always
large-tests:
extends: .python-base
stage: test
rules:
- if: $CI_COMMIT_BRANCH == "main"
- if: $CI_PIPELINE_SOURCE == "schedule"
needs: [small-tests, medium-tests]
script:
- uv run pytest -m large \
--test-categories-enforcement=strict \
--test-size-report=json \
--test-size-report-file=large-tests-report.json \
--junitxml=junit-large.xml \
-v
artifacts:
reports:
junit: junit-large.xml
paths:
- large-tests-report.json
when: always
# ===========================================================================
# Coverage Stage
# ===========================================================================
coverage:
extends: .python-base
stage: coverage
needs: [small-tests, medium-tests]
script:
- uv run coverage run -m pytest \
--test-categories-enforcement=warn \
--test-size-report=detailed
- uv run coverage report --show-missing
- uv run coverage xml
- uv run coverage html
coverage: '/TOTAL.*\s+(\d+%)$/'
artifacts:
reports:
coverage_report:
coverage_format: cobertura
path: coverage.xml
paths:
- htmlcov/
expire_in: 1 week
Jenkins¶
Declarative Pipeline¶
// Jenkinsfile
pipeline {
agent any
environment {
UV_CACHE_DIR = "${WORKSPACE}/.uv-cache"
}
stages {
stage('Setup') {
steps {
sh 'pip install uv'
sh 'uv sync --all-groups'
}
}
stage('Small Tests') {
steps {
sh '''
uv run pytest -m small \
--test-categories-enforcement=strict \
--test-size-report=json \
--test-size-report-file=small-tests-report.json \
--junitxml=junit-small.xml \
-v
'''
}
post {
always {
junit 'junit-small.xml'
archiveArtifacts artifacts: 'small-tests-report.json'
}
}
}
stage('Medium Tests') {
steps {
sh '''
uv run pytest -m medium \
--test-categories-enforcement=strict \
--test-size-report=json \
--test-size-report-file=medium-tests-report.json \
--junitxml=junit-medium.xml \
-v
'''
}
post {
always {
junit 'junit-medium.xml'
archiveArtifacts artifacts: 'medium-tests-report.json'
}
}
}
stage('Large Tests') {
when {
branch 'main'
}
steps {
sh '''
uv run pytest -m large \
--test-categories-enforcement=strict \
--test-size-report=json \
--test-size-report-file=large-tests-report.json \
--junitxml=junit-large.xml \
-v
'''
}
post {
always {
junit 'junit-large.xml'
archiveArtifacts artifacts: 'large-tests-report.json'
}
}
}
stage('Coverage') {
steps {
sh '''
uv run coverage run -m pytest \
--test-categories-enforcement=warn \
--test-size-report=detailed
uv run coverage report --show-missing
uv run coverage xml
uv run coverage html
'''
}
post {
always {
publishHTML(target: [
allowMissing: false,
alwaysLinkToLastBuild: true,
keepAll: true,
reportDir: 'htmlcov',
reportFiles: 'index.html',
reportName: 'Coverage Report'
])
cobertura coberturaReportFile: 'coverage.xml'
}
}
}
}
post {
always {
cleanWs()
}
}
}
Parallel Execution Pipeline¶
// Jenkinsfile
pipeline {
agent none
stages {
stage('Test') {
parallel {
stage('Small Tests - Python 3.11') {
agent { docker { image 'python:3.11-slim' } }
steps {
sh 'pip install uv && uv sync --all-groups'
sh 'uv run pytest -m small --test-categories-enforcement=strict -v'
}
}
stage('Small Tests - Python 3.12') {
agent { docker { image 'python:3.12-slim' } }
steps {
sh 'pip install uv && uv sync --all-groups'
sh 'uv run pytest -m small --test-categories-enforcement=strict -v'
}
}
stage('Medium Tests') {
agent { docker { image 'python:3.12-slim' } }
steps {
sh 'pip install uv && uv sync --all-groups'
sh 'uv run pytest -m medium --test-categories-enforcement=strict -v'
}
}
}
}
stage('Large Tests') {
when { branch 'main' }
agent { docker { image 'python:3.12-slim' } }
steps {
sh 'pip install uv && uv sync --all-groups'
sh 'uv run pytest -m large --test-categories-enforcement=strict -v'
}
}
}
}
Coverage Integration¶
Codecov with pytest-test-categories¶
# .github/workflows/coverage.yml
name: Coverage
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
coverage:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install dependencies
run: |
pip install uv
uv sync --all-groups
- name: Run tests with coverage
run: |
uv run coverage run -m pytest \
--test-categories-enforcement=warn \
--test-size-report=detailed
uv run coverage xml
- name: Upload to Codecov
uses: codecov/codecov-action@v4
with:
files: coverage.xml
flags: unittests
fail_ci_if_error: true
Codecov Configuration¶
# codecov.yml
coverage:
precision: 2
round: down
range: "70...100"
status:
project:
default:
target: auto
threshold: 1%
patch:
default:
target: auto
threshold: 1%
flags:
unittests:
paths:
- src/
carryforward: true
Gradual Migration CI¶
For projects migrating to pytest-test-categories:
# .github/workflows/migration.yml
name: Migration Tests
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
# Warn mode for all tests - see violations but don't fail
test-with-warnings:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install dependencies
run: pip install -e .[test]
- name: Run tests (warn mode)
run: |
pytest --test-categories-enforcement=warn \
--test-size-report=detailed 2>&1 | tee test-output.txt
- name: Check for violations
run: |
if grep -q "HermeticityViolation" test-output.txt; then
echo "::warning::Hermeticity violations detected - see test output"
grep "HermeticityViolation" test-output.txt
fi
# Strict mode for small tests only
small-tests-strict:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install dependencies
run: pip install -e .[test]
- name: Run small tests (strict)
run: |
pytest -m small --test-categories-enforcement=strict -v