Módulo 5: Coverage, TDD y CI
Proyecto: pipeline CI con tests + coverage
Cierre del Módulo 5. Las cápsulas 01-07 te dieron las técnicas; esta cápsula las integra en un mini-proyecto real que combina:
- pytest-cov con line + branch coverage (caps 02-04)
--cov-fail-under=80threshold enforcement (cap 04)- TDD discipline documentada en CONTRIBUTING (cap 05)
- GitHub Actions workflow con services + matrix + caching (cap 06)
- Codecov integration con comments en PRs (cap 07)
- Branch protection rules que bloquean merges sin tests (cap 07)
Al cerrar, vas a tener un pipeline production-grade que:
- Corre tests automáticamente en cada push y PR
- Falla CI si coverage drops debajo de 80%
- Comenta en cada PR con coverage diff
- Bloquea merges si tests fallan o coverage drops
- Genera badges visibles en README
Es el estado production-grade que vas a aplicar al proyecto final del Módulo 6.
El alcance: setup completo
Vamos a configurar para una API FastAPI:
my-api/
├── .github/
│ ├── workflows/
│ │ └── test.yml # CI workflow
│ └── pull_request_template.md # PR template
├── codecov.yml # Codecov config
├── pyproject.toml # pytest-cov config
├── README.md # con badges
├── CONTRIBUTING.md # TDD workflow docs
└── src/
└── app/
└── ...
Paso 1: pyproject.toml completo
[project]
name = "my-api"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = [
"fastapi>=0.110",
"sqlalchemy>=2.0",
"asyncpg>=0.29",
"psycopg2-binary>=2.9",
"pydantic[email]>=2.6",
"pydantic-settings>=2.2",
"PyJWT>=2.8",
"pwdlib[bcrypt]>=0.2",
]
[project.optional-dependencies]
test = [
"pytest>=8.2",
"pytest-cov>=5.0",
"pytest-mock>=3.12",
"pytest-asyncio>=0.23",
"pytest-sugar>=1.0",
"pytest-randomly>=3.15",
"pytest-clarity>=1.0",
"factory-boy>=3.3",
"pytest-factoryboy>=2.7",
"respx>=0.21",
"testcontainers[postgres]>=4.0",
"httpx>=0.27",
]
dev = [
"ruff>=0.4",
"mypy>=1.10",
]
[build-system]
requires = ["setuptools>=68"]
build-backend = "setuptools.build_meta"
[tool.setuptools.packages.find]
where = ["src"]
# ============================================================
# pytest configuration
# ============================================================
[tool.pytest.ini_options]
minversion = "8.0"
testpaths = ["tests"]
pythonpath = ["src"]
addopts = [
"-ra",
"--strict-markers",
"--strict-config",
"--showlocals",
"--cov=app",
"--cov-branch",
"--cov-report=term-missing",
"--cov-report=html:htmlcov",
"--cov-report=xml:coverage.xml",
"--cov-fail-under=80",
]
markers = [
"unit: tests aislados",
"integration: tests con DB",
"e2e: tests HTTP completos",
"slow: > 1s",
]
asyncio_mode = "auto"
filterwarnings = [
"error",
"ignore::DeprecationWarning:asyncpg",
]
# ============================================================
# Coverage configuration
# ============================================================
[tool.coverage.run]
branch = true
source = ["src/app"]
omit = [
"*/tests/*",
"*/migrations/*",
"*/__init__.py",
"*/conftest.py",
]
[tool.coverage.report]
exclude_lines = [
"pragma: no cover",
"raise NotImplementedError",
"if __name__ == .__main__.:",
"if TYPE_CHECKING:",
"@(abc\\.)?abstractmethod",
]
show_missing = true
precision = 2
skip_covered = false
[tool.coverage.html]
directory = "htmlcov"
[tool.coverage.xml]
output = "coverage.xml"
# ============================================================
# Ruff (linting)
# ============================================================
[tool.ruff]
line-length = 100
target-version = "py312"
[tool.ruff.lint]
select = ["E", "F", "W", "I", "N", "UP", "S", "B", "C4", "RUF"]
ignore = [
"E501", # line too long (handled by formatter)
"S101", # use of assert (OK in tests)
]
[tool.ruff.lint.per-file-ignores]
"tests/**" = ["S101", "S105", "S106"] # asserts y dummy passwords OK in tests
# ============================================================
# mypy (type checking)
# ============================================================
[tool.mypy]
python_version = "3.12"
strict = true
ignore_missing_imports = true
Paso 2: GitHub Actions workflow
# .github/workflows/test.yml
name: tests
on:
push:
branches: [main]
pull_request:
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
FORCE_COLOR: 1
PYTHONUNBUFFERED: 1
jobs:
lint:
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
cache: "pip"
cache-dependency-path: pyproject.toml
- name: Install dev dependencies
run: |
pip install --upgrade pip
pip install -e ".[dev]"
- name: Lint with ruff
run: ruff check src tests
- name: Type check with mypy
run: mypy src
test:
needs: lint
runs-on: ubuntu-latest
timeout-minutes: 15
strategy:
fail-fast: false
matrix:
python-version: ["3.12", "3.13"]
services:
postgres:
image: postgres:16-alpine
env:
POSTGRES_PASSWORD: test
POSTGRES_DB: test_db
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v4
- name: Setup Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
cache: "pip"
cache-dependency-path: pyproject.toml
- name: Install dependencies
run: |
pip install --upgrade pip
pip install -e ".[test]"
- name: Run unit tests
run: pytest -m "unit" --cov=app --cov-branch --cov-report=xml --cov-report=term
- name: Run integration tests
env:
TEST_DATABASE_URL: postgresql://postgres:test@localhost:5432/test_db
run: pytest -m "integration" --cov=app --cov-append --cov-report=xml
- name: Run e2e tests
env:
TEST_DATABASE_URL: postgresql://postgres:test@localhost:5432/test_db
run: pytest -m "e2e" --cov=app --cov-append --cov-report=xml
- name: Final coverage report
run: |
coverage report --fail-under=80
coverage html
- name: Upload coverage to Codecov
if: matrix.python-version == '3.12'
uses: codecov/codecov-action@v4
with:
file: ./coverage.xml
fail_ci_if_error: false
- name: Upload artifacts on failure
if: failure()
uses: actions/upload-artifact@v4
with:
name: pytest-results-${{ matrix.python-version }}
path: |
htmlcov/
.pytest_cache/
coverage.xml
retention-days: 7
status:
needs: [lint, test]
runs-on: ubuntu-latest
if: always()
steps:
- name: All checks passed
run: |
if [[ "${{ needs.lint.result }}" != "success" ]] || \
[[ "${{ needs.test.result }}" != "success" ]]; then
echo "One or more checks failed"
exit 1
fi
echo "All checks passed!"
Lo que hace este workflow:
lintcorre primero (rápido) — ruff + mypytestcorre después si lint pasa, en matrix Python 3.12 + 3.13- Tests separados por marker (unit → integration → e2e) con
--cov-append - Codecov upload solo desde Python 3.12 (evitar duplicates)
- Artifacts upload si tests fallan
- Job
statusfinal que pasa solo si lint + test pasaron
Paso 3: codecov.yml
# codecov.yml
coverage:
precision: 2
round: down
range: "70...90"
status:
project:
default:
target: auto
threshold: 0.5%
patch:
default:
target: 80%
comment:
layout: "reach,diff,flags,files"
behavior: default
require_changes: true
show_carryforward_flags: false
flags:
unit:
paths:
- src/app/
carryforward: true
integration:
paths:
- src/app/
carryforward: true
e2e:
paths:
- src/app/
carryforward: true
ignore:
- "tests/**"
- "migrations/**"
- "**/__init__.py"
- "src/app/cli/**"
- "src/app/admin/internal/**"
Paso 4: README con badges
# My API
[](https://github.com/USER/REPO/actions/workflows/test.yml)
[](https://codecov.io/gh/USER/REPO)
[](https://www.python.org)
[](https://github.com/astral-sh/ruff)
A REST API for ...
## Quick start
\`\`\`bash
pip install -e ".[test]"
pytest
\`\`\`
## Coverage
Coverage threshold: **80%** (line + branch).
See [coverage report](https://codecov.io/gh/USER/REPO).
## Contributing
See [CONTRIBUTING.md](./CONTRIBUTING.md) for development workflow.
Paso 5: CONTRIBUTING.md con TDD workflow
# Contributing
## Development workflow
### Setup
\`\`\`bash
git clone https://github.com/USER/REPO.git
cd REPO
pip install -e ".[test,dev]"
\`\`\`
### TDD workflow
We use TDD for business logic. The cycle:
1. **🔴 RED:** write a failing test
2. **🟢 GREEN:** implement the minimum to pass
3. **🔵 REFACTOR:** improve code while tests stay green
Example commit pattern:
\`\`\`bash
git commit -m "test: validate_password rejects short passwords"
git commit -m "feat: implement min length validation"
git commit -m "test: validate_password rejects long passwords"
git commit -m "feat: implement max length validation"
git commit -m "refactor: extract validation rules to list"
\`\`\`
### Running tests
\`\`\`bash
# All tests
pytest
# Fast feedback (skip slow)
pytest -m "not slow"
# Specific marker
pytest -m "unit"
pytest -m "integration"
# With HTML coverage
pytest --cov-report=html
open htmlcov/index.html
\`\`\`
### Coverage requirements
- Project total: 80% minimum (line + branch)
- New code in PR: 80% minimum (`patch` check in Codecov)
### PR checklist
Before opening PR:
- [ ] Tests added/updated
- [ ] Coverage maintained or improved
- [ ] Linting clean (`ruff check`)
- [ ] Type checking clean (`mypy src`)
- [ ] No secrets in code
- [ ] CHANGELOG updated if user-facing change
CI will verify all of these automatically.
Paso 6: PR template
<!-- .github/pull_request_template.md -->
## Description
<!-- What does this PR do? Why? -->
## Type of change
- [ ] Bug fix (regression test added)
- [ ] New feature (with TDD)
- [ ] Refactor (tests pass before and after)
- [ ] Documentation
- [ ] Performance improvement
## Testing
- [ ] Unit tests added/updated
- [ ] Integration tests added/updated (if applicable)
- [ ] E2E tests added/updated (if applicable)
- [ ] Manual testing performed
## Coverage
- [ ] Coverage maintained or improved
- [ ] New business logic has corresponding tests
## Checklist
- [ ] Code follows project style (`ruff check` clean)
- [ ] Type hints clean (`mypy src` clean)
- [ ] Tests pass locally
- [ ] No secrets / credentials in code
- [ ] Documentation updated if needed
- [ ] CHANGELOG updated for user-facing changes
## Additional context
<!-- Screenshots, context for reviewers, related issues -->
Paso 7: branch protection rules
GitHub UI: Settings → Branches → Branch protection rules → Add rule
Branch name pattern: main
✅ Require a pull request before merging
✅ Require approvals: 1
✅ Dismiss stale pull request approvals when new commits are pushed
✅ Require review from Code Owners (if applicable)
✅ Require status checks to pass before merging
✅ Require branches to be up to date before merging
Required status checks:
✅ lint
✅ test (3.12)
✅ test (3.13)
✅ status
✅ codecov/project
✅ codecov/patch
✅ Require conversation resolution before merging
✅ Require signed commits (optional, recommended)
✅ Do not allow bypassing the above settings
✅ Allow administrators to bypass: NO
Paso 8: verificación
Test 1: tests rojos bloquean merge
# Branch con tests rojos
git checkout -b feature/broken
# ... código que rompe tests ...
git push -u origin feature/broken
# Open PR
Resultado esperado:
- CI corre y falla
- PR muestra ❌ "1 failing check"
- Merge button disabled
- Codecov bot comenta "Coverage drop detected"
Test 2: coverage drop bloquea merge
git checkout -b feature/lower-coverage
# Agregas código sin tests
git push -u origin feature/lower-coverage
# Open PR
Resultado esperado:
- Tests pasan, pero
--cov-fail-under=80falla en CI - Codecov comenta con coverage drop específico
- Merge button disabled
Test 3: PR válido se mergea
git checkout -b feature/with-tests
# Agregas feature con tests + coverage maintained
git push -u origin feature/with-tests
# Open PR
Resultado esperado:
- CI verde (lint + test 3.12 + test 3.13 + status + codecov)
- Codecov comenta "Coverage maintained at 87.5%"
- 1 approval recibido
- Merge button enabled
Análisis: lo que tienes ahora
✅ M01-M04 (testing técnico)
- Unit tests con mocks (M02)
- Integration tests con DB real (M03)
- E2E tests con TestClient (M04)
✅ M05 (coverage + TDD + CI — este módulo)
- Coverage measurement con line + branch
- --cov-fail-under=80 enforce threshold
- TDD workflow documented in CONTRIBUTING
- GitHub Actions con lint + matrix + services
- Codecov integration con comments en PRs
- Branch protection con required checks
- PR template estandarizado
Resultado: test suite profesional con automation completa.
Métricas operacionales
Después de unas semanas con este setup:
Métricas que vas a tener:
- Coverage trend (Codecov dashboard)
- Tiempo promedio de PR check (GitHub Actions)
- Número de PRs bloqueados por tests vs por coverage
- Files con menor coverage (Codecov)
- Tests más lentos (--durations en pytest)
Métricas saludables:
- Coverage: 80-90% line, 70-85% branch
- CI duration: < 10 min total
- Test failures: < 5% de runs (mayormente tests flaky a fixear)
- Coverage trend: estable o creciendo
Métricas warning:
- Coverage decreciendo durante 2+ semanas → focar en tests
- CI duration > 15 min → optimizar (caching, parallel)
- Tests flaky frecuentes → investigar (race conditions)
- Coverage drops grandes en PRs → revisar review process
Resumen del Módulo 5
Cap 01 → Marco mental: las 3 disciplinas
Cap 02 → pytest-cov setup y reportes
Cap 03 → Interpretar coverage (line vs branch)
Cap 04 → Coverage thresholds y exclusions
Cap 05 → TDD workflow red-green-refactor
Cap 06 → GitHub Actions workflow básico
Cap 07 → PR checks + Codecov + badges
Cap 08 → Mini-proyecto integrador (este)
Tienes:
- Coverage measurement automático en cada test run
- Threshold enforcement que falla CI si coverage drops
- TDD discipline documentada para el equipo
- GitHub Actions con lint + tests matrix + services
- Codecov integration con comments por PR
- Branch protection que bloquea merges sin tests
- README badges visibles
- PR template estandarizado
- CONTRIBUTING que onboardea developers nuevos
Checkpoint del módulo completo
Antes de avanzar al Módulo 6 (Proyecto Final), deberías poder:
- ✅ Configurar pytest-cov con line + branch coverage
- ✅ Aplicar
--cov-fail-under=80enforced en CI - ✅ Ejecutar un ciclo TDD red-green-refactor
- ✅ Escribir GitHub Actions workflow con services + matrix
- ✅ Setup Codecov con comments en PRs
- ✅ Configurar branch protection con required checks
- ✅ Aplicar todo lo anterior a tu proyecto real
Puente al Módulo 6
Tienes:
- 5 módulos de técnica: pytest, mocking, integration, E2E, coverage/CI
- 44 cápsulas cubriendo cada aspecto
El Módulo 6 es el capstone: construir un test suite production-grade completo para una API real, integrando TODO lo aprendido. Vas a:
- Tomar una API existente (o usar la del path)
- Aplicar test pyramid: 70% unit + 25% integration + 5% e2e
- Coverage 80%+ enforced
- CI pipeline completo
- Documentación de testing strategy
- Reporte presentable como portfolio piece
M06 es donde internalizas todo porque lo haces end-to-end en un proyecto real, no aislado.
Recursos
- pytest-cov docs — referencia
- GitHub Actions docs — referencia
- Codecov docs — referencia
- Kent Beck — TDD — el libro de TDD
- Continuous Delivery — Jez Humble — perspectiva amplia de CI
- Cosmic Python — patterns de architecture testeable
- Real Python — Testing — overview comprehensive