Módulo 2: Testing Automatizado en CI
8. Proyecto — Automated Test Pipeline
Descripción del proyecto
Es hora de integrar todo lo que aprendiste en este módulo en un pipeline de testing profesional. Vas a construir un workflow que corre tests automáticamente en 3 versiones de Python, usa dependency caching para ser rápido, genera JUnit XML reports y coverage reports, y tiene timeouts configurados para proteger contra tests colgados.
El resultado no es un ejercicio académico — es un pipeline de testing real que vivirá en tu repositorio y protegerá tu código en cada push. Este pipeline es la base sobre la cual agregarás AI-specific checks en el Módulo 3.
Objetivo del proyecto
Construir un pipeline de testing automatizado con GitHub Actions que incluya matrix testing, dependency caching, test reports, coverage, y timeouts.
Prerequisitos
- ✅ Completaste las cápsulas 01-07 de este módulo
- ✅ Tienes el proyecto del Módulo 1 con tests funcionando
- ✅ Tienes acceso a la pestaña Actions de tu repo en GitHub
Recap: Lo que construiste en el Módulo 1
Antes de arrancar, asegúrate de tener claro qué tienes hasta ahora. En el proyecto del Módulo 1 construiste:
- Un repositorio con estructura
src/ytests/ src/main.pycon funcionesgreetyclassify_sentimentque simulan un servicio AI básicosrc/utils.pycon funciones de utilidad:estimate_tokens,calculate_cost, yvalidate_prompt- Tests unitarios básicos en
tests/test_main.pyytests/test_utils.py - Un workflow
ci.ymlsimple que corríapytesten una sola versión de Python - Un
requirements.txtcon las dependencias del proyecto
Ese pipeline funcionaba, pero era frágil. No probaba en múltiples versiones de Python, no generaba reports, no tenía caching, y no separaba unit tests de integration tests. En este proyecto vas a tomar esa base y convertirla en un pipeline de testing profesional.
Especificaciones técnicas
Estructura del proyecto
Usas el mismo proyecto del Módulo 1, con estas adiciones:
mi-proyecto-ai/
├── .github/
│ └── workflows/
│ └── ci.yml ← Actualizado con todo lo nuevo
├── src/
│ ├── __init__.py
│ ├── main.py
│ └── utils.py
├── tests/
│ ├── __init__.py
│ ├── unit/ ← NEW: tests organizados
│ │ ├── __init__.py
│ │ ├── test_main.py
│ │ └── test_utils.py
│ └── integration/ ← NEW: tests que simularían API calls
│ ├── __init__.py
│ └── test_ai_service.py
├── requirements.txt ← Actualizado
├── requirements-dev.txt ← NEW: dependencias de desarrollo
├── pyproject.toml ← Actualizado
└── README.md
Código fuente (del Módulo 1)
Si por alguna razón no tienes estos archivos, aquí está el código que los tests esperan. Si ya los tienes del Módulo 1, verifica que las firmas de las funciones coincidan.
src/main.py
"""Módulo principal del servicio AI."""
POSITIVE_WORDS = {"great", "amazing", "excellent", "wonderful", "love", "fantastic", "awesome"}
NEGATIVE_WORDS = {"terrible", "awful", "bad", "worst", "hate", "horrible", "disappointed"}
def greet(name: str) -> str:
"""Genera un saludo para el usuario del servicio."""
cleaned = name.strip()
if not cleaned:
return "Hello, anonymous! Welcome to the AI service."
return f"Hello, {cleaned}! Welcome to the AI service."
def classify_sentiment(text: str) -> dict:
"""Clasifica el sentimiento de un texto usando keyword matching.
En un servicio real esto llamaría a un LLM. Aquí usamos una
implementación local para que los tests no dependan de APIs externas.
"""
if not text or not text.strip():
raise ValueError("Text cannot be empty")
words = set(text.lower().split())
pos_count = len(words & POSITIVE_WORDS)
neg_count = len(words & NEGATIVE_WORDS)
if pos_count > neg_count:
sentiment = "positive"
confidence = min(0.5 + pos_count * 0.15, 1.0)
elif neg_count > pos_count:
sentiment = "negative"
confidence = min(0.5 + neg_count * 0.15, 1.0)
else:
sentiment = "neutral"
confidence = 0.5
return {
"text": text,
"sentiment": sentiment,
"confidence": confidence,
}
src/utils.py
"""Funciones de utilidad para el servicio AI."""
MODEL_PRICING = {
"gpt-4o-mini": {"input": 0.150 / 1_000_000, "output": 0.600 / 1_000_000},
"gpt-4o": {"input": 2.50 / 1_000_000, "output": 10.00 / 1_000_000},
}
def estimate_tokens(text: str) -> int:
"""Estima la cantidad de tokens en un texto.
Usa la heurística de ~4 caracteres por token. No es exacto,
pero es suficiente para estimaciones de costo.
"""
if not text:
return 0
return max(1, len(text) // 4)
def calculate_cost(
prompt_tokens: int, completion_tokens: int, model: str
) -> dict:
"""Calcula el costo estimado de una llamada a un modelo."""
if model not in MODEL_PRICING:
raise ValueError(f"Unknown model: {model}")
pricing = MODEL_PRICING[model]
input_cost = prompt_tokens * pricing["input"]
output_cost = completion_tokens * pricing["output"]
return {
"model": model,
"prompt_tokens": prompt_tokens,
"completion_tokens": completion_tokens,
"input_cost": round(input_cost, 8),
"output_cost": round(output_cost, 8),
"total_cost": round(input_cost + output_cost, 8),
}
def validate_prompt(prompt: str, max_tokens: int = 8000) -> dict:
"""Valida un prompt antes de enviarlo a un modelo."""
issues = []
if not prompt or not prompt.strip():
issues.append("Prompt is empty")
elif len(prompt.strip()) < 10:
issues.append("Prompt is too short (minimum 10 characters)")
estimated = estimate_tokens(prompt)
if estimated > max_tokens:
issues.append(f"Prompt is too long: ~{estimated} tokens (max {max_tokens})")
return {
"valid": len(issues) == 0,
"issues": issues,
"estimated_tokens": estimated,
}
Archivos nuevos y actualizados
requirements-dev.txt
-r requirements.txt
pytest>=8.0
pytest-cov>=5.0
pytest-timeout>=2.3
ruff>=0.3.0
tests/unit/__init__.py
tests/unit/test_main.py
"""Unit tests para el módulo principal."""
import pytest
from src.main import greet, classify_sentiment
class TestGreet:
def test_greet_with_name(self):
assert greet("Alice") == "Hello, Alice! Welcome to the AI service."
def test_greet_empty_string(self):
result = greet("")
assert "anonymous" in result
def test_greet_whitespace_only(self):
result = greet(" ")
assert "anonymous" in result
def test_greet_strips_whitespace(self):
result = greet(" Bob ")
assert "Bob" in result
assert " Bob " not in result
def test_greet_special_characters(self):
result = greet("María José")
assert "María José" in result
class TestClassifySentiment:
def test_positive_sentiment(self):
result = classify_sentiment("This is great and amazing!")
assert result["sentiment"] == "positive"
assert result["confidence"] > 0.5
def test_negative_sentiment(self):
result = classify_sentiment("This is terrible and awful!")
assert result["sentiment"] == "negative"
assert result["confidence"] > 0.5
def test_neutral_sentiment(self):
result = classify_sentiment("The sky is blue.")
assert result["sentiment"] == "neutral"
assert result["confidence"] == 0.5
def test_empty_text_raises(self):
with pytest.raises(ValueError, match="Text cannot be empty"):
classify_sentiment("")
def test_result_structure(self):
result = classify_sentiment("Hello world")
assert "text" in result
assert "sentiment" in result
assert "confidence" in result
def test_confidence_range(self):
result = classify_sentiment("great amazing excellent love")
assert 0 <= result["confidence"] <= 1.0
def test_result_includes_original_text(self):
text = "Test input text"
result = classify_sentiment(text)
assert result["text"] == text
tests/unit/test_utils.py
"""Unit tests para funciones de utilidad."""
import pytest
from src.utils import estimate_tokens, calculate_cost, validate_prompt
class TestEstimateTokens:
def test_empty_string(self):
assert estimate_tokens("") == 0
def test_short_text(self):
result = estimate_tokens("Hello")
assert result >= 1
def test_longer_text(self):
text = "This is a longer piece of text for token estimation."
result = estimate_tokens(text)
assert 10 <= result <= 20
def test_returns_integer(self):
assert isinstance(estimate_tokens("test"), int)
def test_minimum_one_token(self):
assert estimate_tokens("Hi") >= 1
class TestCalculateCost:
def test_gpt4o_mini_cost(self):
result = calculate_cost(1000, 500, "gpt-4o-mini")
assert result["model"] == "gpt-4o-mini"
assert result["total_cost"] > 0
def test_gpt4o_cost(self):
result = calculate_cost(1000, 500, "gpt-4o")
assert result["total_cost"] > calculate_cost(1000, 500, "gpt-4o-mini")["total_cost"]
def test_unknown_model_raises(self):
with pytest.raises(ValueError, match="Unknown model"):
calculate_cost(100, 100, "gpt-5-imaginary")
def test_zero_tokens(self):
result = calculate_cost(0, 0, "gpt-4o-mini")
assert result["total_cost"] == 0
def test_cost_structure(self):
result = calculate_cost(1000, 500, "gpt-4o")
required_keys = [
"model", "prompt_tokens", "completion_tokens",
"input_cost", "output_cost", "total_cost",
]
for key in required_keys:
assert key in result
def test_cost_proportional_to_tokens(self):
cost_1k = calculate_cost(1000, 500, "gpt-4o-mini")["total_cost"]
cost_2k = calculate_cost(2000, 1000, "gpt-4o-mini")["total_cost"]
assert abs(cost_2k - cost_1k * 2) < 0.0001
class TestValidatePrompt:
def test_valid_prompt(self):
result = validate_prompt("Summarize the following text in 3 sentences.")
assert result["valid"] is True
assert len(result["issues"]) == 0
def test_empty_prompt(self):
result = validate_prompt("")
assert result["valid"] is False
def test_short_prompt(self):
result = validate_prompt("Hi")
assert result["valid"] is False
def test_token_estimation_included(self):
result = validate_prompt("A normal prompt for testing purposes.")
assert "estimated_tokens" in result
assert result["estimated_tokens"] > 0
def test_very_long_prompt(self):
long_prompt = "word " * 20000
result = validate_prompt(long_prompt, max_tokens=4000)
assert result["valid"] is False
assert any("too long" in issue.lower() for issue in result["issues"])
tests/integration/__init__.py
tests/integration/test_ai_service.py
"""
Integration tests que simulan interacciones con servicios AI.
En un proyecto real, estos tests llamarían a APIs externas.
Aquí simulamos el patrón para demostrar timeouts y separación.
"""
import time
import pytest
from src.main import classify_sentiment
from src.utils import estimate_tokens, calculate_cost
@pytest.mark.integration
@pytest.mark.timeout(10)
class TestAIServiceIntegration:
"""Tests que simulan llamadas a servicios AI."""
def test_sentiment_pipeline(self):
"""Simula el pipeline completo: tokenize → classify → cost."""
text = "This product is absolutely amazing and wonderful!"
tokens = estimate_tokens(text)
sentiment = classify_sentiment(text)
cost = calculate_cost(tokens, tokens // 2, "gpt-4o-mini")
assert sentiment["sentiment"] == "positive"
assert cost["total_cost"] > 0
assert cost["total_cost"] < 0.01
def test_batch_processing(self):
"""Simula procesamiento batch de múltiples textos."""
texts = [
"Great product, love it!",
"Terrible experience, worst ever.",
"The weather is nice today.",
"Amazing service, highly recommend!",
"Bad quality, very disappointed.",
]
results = [classify_sentiment(text) for text in texts]
positive = sum(1 for r in results if r["sentiment"] == "positive")
negative = sum(1 for r in results if r["sentiment"] == "negative")
assert positive >= 2
assert negative >= 2
def test_cost_estimation_batch(self):
"""Simula estimación de costos para un batch de requests."""
texts = ["Sample text for estimation."] * 100
total_cost = 0
for text in texts:
tokens = estimate_tokens(text)
cost = calculate_cost(tokens, tokens, "gpt-4o-mini")
total_cost += cost["total_cost"]
assert total_cost > 0
assert total_cost < 1.0
@pytest.mark.timeout(5)
def test_with_simulated_latency(self):
"""Simula un test con latencia de API (timeout test)."""
time.sleep(0.5)
result = classify_sentiment("Quick test with simulated latency")
assert result["sentiment"] in ["positive", "negative", "neutral"]
pyproject.toml (actualizado)
[tool.ruff]
target-version = "py310"
line-length = 88
[tool.ruff.lint]
select = ["E", "F", "I", "W"]
[tool.pytest.ini_options]
testpaths = ["tests"]
pythonpath = ["."]
markers = [
"integration: tests that simulate external API calls",
"slow: tests that take more than 5 seconds",
]
timeout = 30
[tool.coverage.run]
source = ["src"]
omit = ["tests/*"]
[tool.coverage.report]
show_missing = true
fail_under = 80
El Workflow: ci.yml completo
Este es el workflow que integra todo lo aprendido en el módulo:
name: CI Pipeline
on:
push:
branches: [main]
paths-ignore:
- "*.md"
- "docs/**"
pull_request:
branches: [main]
workflow_dispatch:
env:
MIN_COVERAGE: 80
jobs:
# ──────────── JOB 1: LINT ────────────
lint:
name: Code Quality
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: "3.12"
cache: "pip"
- name: Install linting tools
run: pip install ruff
- name: Run linting
run: ruff check src/ tests/
- name: Check formatting
run: ruff format --check src/ tests/
# ──────────── JOB 2: UNIT TESTS (MATRIX) ────────────
unit-tests:
name: Unit Tests (Python ${{ matrix.python-version }})
runs-on: ubuntu-latest
timeout-minutes: 10
strategy:
fail-fast: false
matrix:
python-version: ["3.10", "3.11", "3.12"]
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
cache: "pip"
- name: Install dependencies
run: pip install -r requirements-dev.txt
- name: Run unit tests with coverage
run: |
pytest tests/unit/ \
-v \
--tb=short \
--timeout=30 \
--junitxml=reports/junit-${{ matrix.python-version }}.xml \
--cov=src \
--cov-report=term-missing \
--cov-report=xml:reports/coverage-${{ matrix.python-version }}.xml
- name: Upload test reports
if: always()
uses: actions/upload-artifact@v4
with:
name: test-reports-py${{ matrix.python-version }}
path: reports/
retention-days: 7
# ──────────── JOB 3: INTEGRATION TESTS ────────────
integration-tests:
name: Integration Tests
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: "3.12"
cache: "pip"
- name: Install dependencies
run: pip install -r requirements-dev.txt
- name: Run integration tests
run: |
pytest tests/integration/ \
-v \
--tb=short \
--timeout=60 \
-m integration \
--junitxml=reports/junit-integration.xml
- name: Upload integration reports
if: always()
uses: actions/upload-artifact@v4
with:
name: integration-reports
path: reports/
retention-days: 7
# ──────────── JOB 4: PIPELINE STATUS ────────────
pipeline-status:
name: Pipeline Status
needs: [lint, unit-tests, integration-tests]
runs-on: ubuntu-latest
timeout-minutes: 5
if: always()
steps:
- name: Check all jobs
run: |
echo "=== Pipeline Results ==="
echo "Lint: ${{ needs.lint.result }}"
echo "Unit Tests: ${{ needs.unit-tests.result }}"
echo "Integration Tests: ${{ needs.integration-tests.result }}"
echo ""
if [ "${{ needs.lint.result }}" != "success" ] || \
[ "${{ needs.unit-tests.result }}" != "success" ] || \
[ "${{ needs.integration-tests.result }}" != "success" ]; then
echo "❌ Pipeline FAILED"
exit 1
fi
echo "✅ All checks passed!"
Arquitectura del pipeline
Push/PR
│
├──► lint ────────────────────────────────────────┐
│ │
├──► unit-tests (Python 3.10) ────────────────────┤
├──► unit-tests (Python 3.11) ────────────────────┤ (todos en paralelo)
├──► unit-tests (Python 3.12) ────────────────────┤
│ │
├──► integration-tests ───────────────────────────┤
│ │
└──► pipeline-status ◄────────────────────────────┘ (espera a todos)
needs: [lint, unit-tests, integration-tests]
6 jobs en paralelo: lint + 3 matrix (unit tests) + integration tests, seguidos de un status check final.
Paso a paso: Implementación
Paso 1: Actualiza la estructura del proyecto
# Crea los directorios de tests organizados
mkdir -p tests/unit tests/integration
# Mueve los tests existentes
mv tests/test_main.py tests/unit/test_main.py
mv tests/test_utils.py tests/unit/test_utils.py
# Crea __init__.py
touch tests/unit/__init__.py
touch tests/integration/__init__.py
Paso 2: Crea los archivos nuevos
Crea requirements-dev.txt, actualiza pyproject.toml, y crea tests/integration/test_ai_service.py con el contenido mostrado arriba.
Paso 3: Verifica localmente
# Instala dependencias de desarrollo
pip install -r requirements-dev.txt
# Corre unit tests
pytest tests/unit/ -v --cov=src --cov-report=term-missing
Output esperado:
tests/unit/test_main.py::TestGreet::test_greet_with_name PASSED
tests/unit/test_main.py::TestGreet::test_greet_empty_string PASSED
... (todos los tests)
tests/unit/test_utils.py::TestValidatePrompt::test_very_long_prompt PASSED
---------- coverage: platform linux, python 3.12.0 ----------
Name Stmts Miss Cover Missing
------------------------------------------------
src/__init__.py 0 0 100%
src/main.py 25 2 92% 42-43
src/utils.py 30 0 100%
------------------------------------------------
TOTAL 55 2 96%
========================= 27 passed in 0.15s =========================
# Corre integration tests
pytest tests/integration/ -v -m integration --timeout=30
Output esperado:
tests/integration/test_ai_service.py::TestAIServiceIntegration::test_sentiment_pipeline PASSED
tests/integration/test_ai_service.py::TestAIServiceIntegration::test_batch_processing PASSED
tests/integration/test_ai_service.py::TestAIServiceIntegration::test_cost_estimation_batch PASSED
tests/integration/test_ai_service.py::TestAIServiceIntegration::test_with_simulated_latency PASSED
========================= 4 passed in 0.62s =========================
# Corre lint
ruff check src/ tests/
ruff format --check src/ tests/
Paso 4: Actualiza el workflow y push
Reemplaza .github/workflows/ci.yml con el workflow completo mostrado arriba.
git add .
git commit -m "Upgrade CI with matrix testing, caching, coverage, and timeouts"
git push origin main
Paso 5: Verifica en GitHub
- Ve a Actions → verás 6 jobs ejecutándose
- lint, 3 unit-tests (matrix), e integration-tests corren en paralelo
- pipeline-status espera a todos
- Descarga los artifacts: reports con JUnit XML y coverage
Checklist de completitud
Workflow
- Matrix testing con Python 3.10, 3.11, 3.12
-
fail-fast: falseen la matrix - Dependency caching con
cache: "pip" - Timeouts en todos los jobs
- JUnit XML reports generados
- Coverage reports generados
- Artifacts uploaded con
actions/upload-artifact - Unit tests y integration tests en jobs separados
- Job final que verifica todos los resultados
Tests
- Unit tests organizados en
tests/unit/ - Integration tests en
tests/integration/con@pytest.mark.integration - Timeouts configurados:
@pytest.mark.timeout()en integration tests - Coverage > 80% en unit tests
- Todos los tests pasan localmente en Python 3.10, 3.11, y 3.12
Configuración
-
pyproject.tomlcon markers, timeout default, y coverage config -
requirements-dev.txtcon pytest, pytest-cov, pytest-timeout, ruff -
.github/workflows/ci.ymlactualizado con el pipeline completo
Troubleshooting del proyecto
"Matrix job falla en Python 3.10 pero pasa en 3.12"
Revisa si tu código usa features de Python 3.11+ (como match statements o ExceptionGroup). El target version en pyproject.toml debe ser py310 si quieres compatibilidad.
"Coverage report muestra < 80%"
Si --cov-fail-under=80 falla, revisa qué líneas no están cubiertas con --cov-report=term-missing. Agrega tests para las líneas faltantes o ajusta el threshold.
"Artifact upload falla"
Verifica que el directorio reports/ existe. Agrégalo antes del step de pytest:
- name: Create reports directory
run: mkdir -p reports
"Integration tests tardan mucho"
El test_with_simulated_latency tiene un time.sleep(0.5). En un proyecto real con API calls, los tiempos pueden ser mayores. Verifica que el timeout-minutes: 15 del job es suficiente.
"Import path issues: ModuleNotFoundError: No module named 'src'"
Este es uno de los errores más comunes. pytest necesita saber dónde buscar el módulo src. La solución está en pyproject.toml:
[tool.pytest.ini_options]
pythonpath = ["."]
Sin esa línea, pytest no encuentra src.main ni src.utils. Si ya la tienes y sigue fallando, verifica que estás corriendo pytest desde la raíz del proyecto (donde está pyproject.toml), no desde dentro de tests/ u otro subdirectorio.
En el workflow de GitHub Actions esto no suele ser problema porque actions/checkout te deja en la raíz del repo. Pero si corres localmente, asegúrate de estar en el directorio correcto.
"Ruff formatting check fails en CI pero pasa local"
El job de lint corre ruff format --check que verifica pero no modifica archivos. Si falla, significa que hay archivos sin formatear. Antes de hacer push:
ruff format src/ tests/
ruff check src/ tests/ --fix
La causa más común es editar archivos sin tener configurado format-on-save en tu editor. Si usas VS Code, asegúrate de tener la extensión de Ruff instalada y activado editor.formatOnSave. Otra causa frecuente: formatear con black en lugar de ruff — ambos producen output ligeramente distinto.
Criterios de éxito
| Criterio | Estado |
|---|---|
| 6 jobs corren en la UI de Actions | ☐ |
| Matrix: 3 versiones de Python en paralelo | ☐ |
| Caching: pip install < 10 segundos (después del primer run) | ☐ |
| Reports: JUnit XML y coverage como artifacts descargables | ☐ |
| Timeouts: configurados en jobs y tests | ☐ |
| Coverage: > 80% en unit tests | ☐ |
| Pipeline status: job final verifica todo | ☐ |
Qué construiste
Al completar este proyecto tienes:
- ✅ Pipeline de testing profesional con matrix testing (3 versiones de Python)
- ✅ Dependency caching que hace tu CI rápido
- ✅ Test reports y coverage visibles como artifacts
- ✅ Timeouts que protegen contra tests colgados
- ✅ Separación clara entre unit tests e integration tests
- ✅ La base para agregar AI-specific checks en el Módulo 3
Extensiones opcionales
Si terminaste el proyecto y quieres ir más allá, aquí hay tres extensiones que refuerzan lo aprendido y hacen tu pipeline más robusto.
1. Agregar mypy para type checking
Agrega un step de type checking estático al job de lint. Esto verifica que los type hints en tu código son consistentes:
- name: Install type checker
run: pip install mypy
- name: Run type checking
run: mypy src/ --ignore-missing-imports
Necesitarás agregar mypy>=1.8 a requirements-dev.txt. mypy puede ser ruidoso al principio — usa --ignore-missing-imports para evitar errores por dependencias externas sin stubs.
2. Agregar pre-commit hooks
pre-commit te permite correr checks antes de cada commit local, atrapando errores antes de que lleguen al CI. Crea .pre-commit-config.yaml en la raíz:
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.3.0
hooks:
- id: ruff
args: [--fix]
- id: ruff-format
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.5.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
Instálalo con pip install pre-commit && pre-commit install. A partir de ahora, cada git commit correrá estos checks automáticamente.
3. Badge de CI en el README
Agrega un badge al inicio de tu README.md que muestre el estado del pipeline:

Reemplaza TU-USUARIO y TU-REPO con tus datos. El badge se actualiza automáticamente — verde si el pipeline pasa, rojo si falla. Es un indicador visual inmediato del estado de salud de tu proyecto.
Siguiente módulo
El Módulo 3 (AI-Specific CI Checks) agrega lo que hace especial a esta guía:
- Prompt regression testing: Detectar que un cambio degradó la calidad de las respuestas
- Cost estimation checks: Calcular cuánto costará un prompt antes de merge
- Quality gates: Bloquear merge si los checks AI fallan
La transición es directa: "Tus tests ya corren automáticamente con matrix, caching, y reports → ahora agreguemos checks que ningún CI genérico tiene."
Recursos adicionales
- Building and testing Python - Guía oficial de Python en Actions
- Using a matrix for your jobs - Referencia de matrix strategy
- Caching dependencies - Guía de caching
- pytest-cov documentation - Plugin de coverage
- pytest-timeout documentation - Plugin de timeouts
- Upload artifact action - Guardar archivos de workflow runs