Testing strategies for continuous integration pipelines.
┌─────────────────────────────────────────────────────────────────┐
│ CI Pipeline │
│ │
│ ┌──────┐ ┌──────┐ ┌───────┐ ┌─────┐ ┌──────┐ ┌────┐│
│ │ Lint │ → │ Unit │ → │ Build │ → │Integ│ → │ E2E │ → │Dep.││
│ │ │ │Tests │ │ │ │Tests│ │Tests │ │ ││
│ └──────┘ └──────┘ └───────┘ └─────┘ └──────┘ └────┘│
│ 1m 2-5m 1-3m 5-10m 10-30m - │
│ │
│ ◄─────── Fast Feedback ───────► ◄─── Comprehensive ──────► │
└─────────────────────────────────────────────────────────────────┘
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Lint
run: |
pip install ruff
ruff check .
unit-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Install dependencies
run: pip install -e .[test]
- name: Run unit tests
run: pytest tests/unit -v --cov=src --cov-report=xml
- name: Upload coverage
uses: codecov/codecov-action@v4
integration-tests:
needs: unit-tests
runs-on: ubuntu-latest
services:
postgres:
image: postgres:15
env:
POSTGRES_PASSWORD: postgres
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Run integration tests
env:
DATABASE_URL: postgres://postgres:postgres@localhost:5432/test
run: pytest tests/integration -v
e2e-tests:
needs: integration-tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run E2E tests
run: |
docker-compose up -d
pytest tests/e2e -v
docker-compose down
- name: Run tests in parallel
run: pytest -n auto # Use all available CPUs
- name: Run with specific workers
run: pytest -n 4 # 4 parallel workers
jobs:
test:
strategy:
matrix:
python-version: ['3.9', '3.10', '3.11']
os: [ubuntu-latest, macos-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- run: pytest
jobs:
test:
strategy:
matrix:
shard: [1, 2, 3, 4]
steps:
- name: Run test shard
run: pytest --shard-id=${{ matrix.shard }} --num-shards=4
- name: Cache pip packages
uses: actions/cache@v4
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements*.txt') }}
restore-keys: |
${{ runner.os }}-pip-
- name: Cache pytest
uses: actions/cache@v4
with:
path: .pytest_cache
key: pytest-${{ github.sha }}
restore-keys: pytest-
- name: Run tests with retry
uses: nick-fields/retry@v3
with:
timeout_minutes: 10
max_attempts: 3
command: pytest tests/e2e
# Rerun failed tests up to 3 times
pytest --reruns 3 --reruns-delay 1
@pytest.mark.flaky(reruns=3, reruns_delay=2)
def test_sometimes_fails():
# This test is known to be flaky
pass
@pytest.mark.skip(reason="Flaky - investigating")
def test_quarantined():
pass
- name: Run tests
run: pytest --junitxml=results.xml
- name: Publish Test Results
uses: dorny/test-reporter@v1
if: always()
with:
name: Test Results
path: results.xml
reporter: java-junit
- name: Run with coverage
run: pytest --cov=src --cov-report=xml --cov-report=html
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v4
with:
files: ./coverage.xml
fail_ci_if_error: true
- name: Coverage comment on PR
uses: py-cov-action/python-coverage-comment-action@v3
# Require tests to pass before merge
# Settings → Branches → Branch protection rules
Required status checks:
- lint
- unit-tests
- integration-tests
Require branches to be up to date: Yes
- name: Get changed files
id: changed
uses: tj-actions/changed-files@v41
with:
files: |
src/**
tests/**
- name: Run affected tests
if: steps.changed.outputs.any_changed == 'true'
run: pytest tests/ -v
- name: Quick tests on PR
if: github.event_name == 'pull_request'
run: pytest -m "not slow and not e2e"
- name: Full tests on main
if: github.ref == 'refs/heads/main'
run: pytest
- name: Run tests with secrets
env:
API_KEY: ${{ secrets.TEST_API_KEY }}
DATABASE_URL: ${{ secrets.TEST_DATABASE_URL }}
run: pytest tests/integration
# Use environment for sensitive tests
jobs:
integration:
environment: testing # Requires approval
steps:
- run: pytest tests/integration
pytest -x)