Módulo 1: Testing Fundamentals para AI
8. Troubleshooting y Resumen del Módulo 1
Descripción
Esta es la cápsula de cierre del Módulo 1. Consolida los errores más comunes que encuentran los equipos al configurar testing para apps LLM, con diagnóstico y soluciones concretas. También incluye un resumen de los conceptos clave del módulo y la transición clara hacia el Módulo 2.
Esta cápsula está pensada como referencia rápida: cuando algo falla en tu suite, ven aquí antes de buscar en Google. Los problemas están ordenados por frecuencia, con diagnóstico de causa raíz y solución específica.
Errores más comunes — Diagnóstico y solución
Error 1: ModuleNotFoundError: No module named 'app'
Frecuencia: Muy común en la primera configuración.
Síntoma:
ERRORS
tests/unit/test_parsers.py - ModuleNotFoundError: No module named 'app'
Causa: Python no puede encontrar el paquete app porque src/ no está en el PYTHONPATH.
Soluciones (elige una):
# SOLUCIÓN A: sys.path en conftest.py (la más simple)
# tests/conftest.py
import sys
import os
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'src'))
# Este sys.path.insert hace que 'import app' funcione en todos los tests
# SOLUCIÓN B: pyproject.toml con setup de paquete editable
# pyproject.toml
[build-system]
requires = ["setuptools"]
build-backend = "setuptools.backends.legacy:build"
[tool.setuptools.packages.find]
where = ["src"]
# Instalar en modo editable:
# pip install -e .
# SOLUCIÓN C: pytest.ini con pythonpath (pytest >= 7.0)
# pytest.ini
[pytest]
pythonpath = src
Cuál usar: La Solución C con pythonpath = src en pytest.ini es la más limpia para proyectos nuevos. La Solución A es la más rápida si ya tienes el proyecto configurado.
Error 2: AttributeError: Mock object has no attribute 'choices'
Frecuencia: Muy común al crear mocks de la primera vez.
Síntoma:
AttributeError: Mock object has no attribute 'choices'
# O:
AttributeError: 'dict' object has no attribute 'choices'
Causa 1: La fixture retorna un diccionario Python, pero el código del app accede a la respuesta con dot notation (response.choices).
# ❌ Fixture incorrecta (retorna dict)
@pytest.fixture
def bad_response():
return {"choices": [{"message": {"content": "..."}}]}
# El código hace:
content = response.choices[0].message.content # AttributeError!
# Los dicts usan bracket notation, no dot notation
# ✅ Fixture correcta (retorna MagicMock)
@pytest.fixture
def good_response():
mock = MagicMock()
mock.choices[0].message.content = '{"sentiment": "positive"}'
return mock
Causa 2: MagicMock() crea atributos automáticamente pero listas vacías no son automáticas.
# ❌ Problema: choices[0] accede a una lista vacía
response = MagicMock()
response.choices[0].message.content = "..." # No funciona directamente
# ✅ Solución: asignar una lista real con un MagicMock
response = MagicMock()
choice = MagicMock()
choice.message.content = '{"sentiment": "positive"}'
response.choices = [choice] # Lista real con MagicMock
Diagnóstico rápido:
# Ejecutar en Python interactivo para verificar tu mock:
from unittest.mock import MagicMock
mock = MagicMock()
choice = MagicMock()
choice.message.content = "test content"
mock.choices = [choice]
print(mock.choices[0].message.content) # Debe imprimir "test content"
Error 3: El mock no se aplica — el código llama a la API real
Frecuencia: Muy común cuando @patch tiene el path incorrecto.
Síntoma:
# El test pasa pero ves cobros en la API key
# O el test es lento (3-5 segundos en vez de <100ms)
Causa: El path del @patch no coincide con dónde se importa el objeto.
# ❌ Path incorrecto: parchea donde se DEFINE
@patch("openai.Client.chat.completions.create")
def test_algo(mock_create):
... # No funciona: el código importa desde su módulo, no desde openai
# ✅ Path correcto: parchea donde se USA
@patch("app.sentiment.client.chat.completions.create")
def test_algo(mock_create):
... # Funciona: intercepta la llamada en el módulo que la hace
Regla de oro: El path del @patch debe ser "módulo_donde_se_usa.objeto.método".
Cómo encontrar el path correcto:
# 1. Abre el módulo que llama al LLM (ej: app/sentiment.py)
# 2. Encuentra la línea donde se importa el cliente:
from openai import OpenAI
client = OpenAI() # El objeto 'client' está en app.sentiment
# 3. El path correcto es:
# @patch("app.sentiment.client.chat.completions.create")
Verificación:
@patch("app.sentiment.client.chat.completions.create")
def test_no_real_api_calls(mock_create, sentiment_positive_response):
mock_create.return_value = sentiment_positive_response
analyze_sentiment("test")
# Si esto pasa en <100ms, el mock está funcionando
# Si tarda >2 segundos, el mock no se aplicó
mock_create.assert_called_once()
Error 4: Tests flaky — pasan a veces, fallan otras
Frecuencia: Común cuando se usan LLMs reales en unit tests.
Síntoma:
FAILED tests/unit/test_sentiment.py::test_positive_sentiment
# Pasa 7 de 10 veces
Causa: El test llama al LLM real (no mockeado) y el output varía.
Diagnóstico:
# Correr el mismo test múltiples veces
pytest tests/unit/test_sentiment.py::test_positive_sentiment -v --count=5
# Si algunos pasan y otros fallan → flaky test por LLM real
Solución:
# ❌ Test flaky: llama al LLM real
def test_positive_sentiment_flaky():
result = analyze_sentiment("I love this!")
assert result["sentiment"] == "positive" # Falla cuando el LLM dice "neutral"
# ✅ Test robusto: mocka el LLM
@patch("app.sentiment.client.chat.completions.create")
def test_positive_sentiment_robust(mock_create, make_llm_response):
mock_create.return_value = make_llm_response(
'{"sentiment": "positive", "confidence": 0.9}'
)
result = analyze_sentiment("I love this!")
assert result["sentiment"] == "positive" # Siempre pasa
Regla: En tests de tipo unit, contract, behavioral, y regression: SIEMPRE mockea el LLM. Solo los tests marcados con @pytest.mark.integration deben llamar a la API real.
Error 5: fixture 'nombre_fixture' not found
Frecuencia: Moderado.
Síntoma:
ERRORS
tests/unit/test_algo.py::test_algo - fixture 'make_llm_response' not found
Causa posible 1: El conftest.py tiene error de sintaxis y no se carga.
# Verificar que conftest.py no tiene errores:
python -c "import tests.conftest"
# Si no imprime nada y no lanza excepción → OK
Causa posible 2: El conftest.py está en el directorio equivocado.
tests/
├── conftest.py ← Disponible para todos los tests en tests/
└── unit/
├── conftest.py ← Disponible solo para tests en tests/unit/
└── test_algo.py ← Puede usar fixtures de AMBOS conftest.py
Causa posible 3: La fixture tiene un typo en el nombre.
# conftest.py
@pytest.fixture
def make_llm_resonse(): # Typo: "resonse" en vez de "response"
...
# test_algo.py
def test_x(make_llm_response): # Busca "response" — no encuentra la fixture
...
Error 6: Tests lentos — suite tarda >30 segundos
Frecuencia: Moderado.
Síntoma: pytest -m "not integration" tarda >30 segundos.
Causa principal: Algún test está llamando a la API real (sin mock).
Diagnóstico:
# Ver los tests más lentos:
pytest -m "not integration" --durations=10
# Output muestra los 10 tests más lentos con su tiempo
# Si alguno tarda >1 segundo → probablemente llama a la API
Solución:
# Identificar qué test es lento
pytest -m "not integration" -v --durations=10
# Correr ese test específico con -s para ver output
pytest tests/unit/test_algo.py::test_lento -s
# Si hace llamadas a URLs externas → no está mockeado correctamente
Prevención: Configura un timeout para tests unitarios:
pip install pytest-timeout
# En pytest.ini:
# timeout = 5 # Falla cualquier test que tarde >5 segundos
Error 7: PytestUnknownMarkWarning: Unknown pytest.mark.contract
Frecuencia: Bajo pero confuso.
Síntoma:
PytestUnknownMarkWarning: Unknown pytest.mark.contract - is this a typo?
Causa: El marker se usa en código pero no está registrado en pytest.ini.
Solución:
# pytest.ini — añadir todos los markers usados
[pytest]
markers =
smoke: Tests de humo
contract: Tests de contrato
behavioral: Tests de comportamiento
regression: Tests de regresión
unit: Tests unitarios
integration: Tests de integración
Error 8: Coverage muestra 0% para módulos que sí tienen tests
Síntoma:
pytest --cov=app --cov-report=term-missing
# Output: "No data to report"
# O coverage muestra 0% para módulos testeados
Causa: El módulo especificado en --cov= no coincide con el nombre del paquete.
Diagnóstico:
# Verificar que app es el nombre correcto del paquete
ls src/
# app/ ← El directorio debe tener __init__.py
python -c "import app; print(app.__file__)"
# Debe imprimir la ruta a src/app/__init__.py
Solución:
# Especificar la ruta correcta
pytest --cov=src/app --cov-report=term-missing
# O configurar en pyproject.toml:
# [tool.coverage.run]
# source = ["app"]
Checklist de cierre del módulo
Antes de pasar al Módulo 2, verifica que tienes:
Configuración base:
├── [ ] pytest.ini con markers registrados y testpaths configurado
├── [ ] conftest.py con fixture make_llm_response (factory)
├── [ ] conftest.py con fixtures de responses predefinidas
├── [ ] conftest.py con mock_openai_client
├── [ ] helpers.py con create_openai_chat_response()
└── [ ] sys.path configurado para importar 'app'
Tests escritos:
├── [ ] ≥3 smoke tests (módulos importan, API responde)
├── [ ] ≥5 contract tests (estructura del output del parser)
├── [ ] ≥3 contract tests (estructura del output de la función principal)
├── [ ] ≥3 behavioral tests (propiedades invariantes)
└── [ ] ≥2 regression tests
Verificación:
├── [ ] pytest -m smoke → ✅ Todo pasa
├── [ ] pytest -m contract → ✅ Todo pasa
├── [ ] pytest -m "not integration" → ✅ Todo pasa
├── [ ] pytest -m "not integration" --durations=5 → Todos <1 segundo
└── [ ] pytest --cov=app -m "not integration" → Coverage >70%
Resumen del módulo: conceptos clave
| Cápsula | Concepto clave | Para recordar |
|---|---|---|
| 01 | Intro y setup | Testing verifica código; Evaluation mide calidad del output LLM |
| 02 | Por qué diferente | 70-80% del código es determinístico. Non-determinism no es excusa |
| 03 | Configuración | pytest.ini + conftest.py + markers + parametrize. Scope de fixtures |
| 04 | Anatomía AAA | Arrange incluye mock. Una Act por test. Assert con mensajes informativos |
| 05 | Fixtures | MagicMock (no dicts). Factory fixtures. AsyncMock para código async |
| 06 | Taxonomía | Smoke → Contract → Behavioral → Regression. Cada uno tiene su propósito |
| 07 | Proyecto | Test Suite Setup completo con todos los tipos de tests |
| 08 | Este módulo | Troubleshooting + checklist + transición al Módulo 2 |
Qué aprendiste y qué puedes hacer ahora
Al completar este módulo puedes:
Explicar:
- Por qué las apps AI necesitan testing diferente al software tradicional
- La diferencia entre testing y evaluation de LLMs
- Cuándo usar cada tipo de test (smoke, contract, behavioral, regression)
Configurar:
- pytest con markers, conftest.py, parametrize y coverage
- Fixtures para mocking del LLM con estructura realista
- Estrategia de tests en 3 niveles (unit/integration/regression)
Escribir:
- Smoke tests para verificar que el sistema arranca
- Contract tests para prompts y parsers (estructura del output)
- Behavioral tests para propiedades invariantes
- Regression tests para bugs conocidos
Ejecutar:
- Suite completa sin costo en <30 segundos
- Subsets según contexto (desarrollo, PR, semanal)
Transición al Módulo 2: Unit Testing LLM Applications
El Módulo 1 configuró la infraestructura. El Módulo 2 la profundiza con tres técnicas avanzadas:
1. Mocking avanzado de respuestas LLM
Módulo 1: create_openai_chat_response() básico
Módulo 2: Mocking de streams, de múltiples llamadas,
de errores específicos de OpenAI API
2. Prompt Contract Tests
Módulo 1: Contract tests básicos (¿tiene las keys?)
Módulo 2: Contract tests completos:
- Validan que el prompt produce la estructura prometida
- Testing de múltiples variaciones del mismo prompt
- Detectar regresiones cuando el prompt cambia
3. Snapshot Testing
Módulo 1: assert result == expected (valor fijo)
Módulo 2: Snapshot testing — guardar el output "dorado" de una función
y verificar que no cambia en el tiempo
4. Testing de parsers y output processors
Módulo 1: Algunos tests de parsers
Módulo 2: Testing exhaustivo de parsers:
- Con todos los formatos posibles de output del LLM
- Edge cases de la API (JSON en markdown, con texto extra)
- Fixture factories especializadas para cada parser
La transición en una frase: "Tienes pytest configurado y entiendes qué testear → ahora aprende a mockear con precisión quirúrgica y a tratar prompts como contratos formales."
Ejercicios de cierre
Ejercicio 1: Diagnóstico de suite existente
Tu suite tiene 20 tests. pytest -m "not integration" tarda 45 segundos y 3 tests fallan intermitentemente. ¿Qué diagnóstico harías y en qué orden?
Ver guía
Paso 1: Identificar qué es lento
pytest -m "not integration" --durations=10
Si algún test tarda >2s → probablemente llama a la API real. Fix: añadir @patch.
Paso 2: Identificar los flaky tests
pytest tests/unit/test_algo.py::test_flaky -v --count=5
Si falla algunos de 5 → flaky por LLM real. Fix: mockear el LLM.
Paso 3: Verificar que el path del patch es correcto
pytest tests/ -v --tb=long
Si los tests que deberían estar mockeados tardan >1s → el mock no se aplica.
Orden de prioridad:
- Fix flaky tests (más impacto en confianza de la suite)
- Fix tests lentos (más impacto en velocidad de desarrollo)
- Fix tests con assertions vagas (más impacto en mantenibilidad)
Ejercicio 2: Mejorar fixture genérica
Esta fixture solo cubre un caso. ¿Cómo la mejorarías para cubrir más sin repetir código?
@pytest.fixture
def llm_response():
mock = MagicMock()
mock.choices[0].message.content = '{"sentiment": "positive", "confidence": 0.9}'
return mock
Ver solución
# ANTES: fixture genérica con valor fijo
@pytest.fixture
def llm_response():
mock = MagicMock()
mock.choices[0].message.content = '{"sentiment": "positive", "confidence": 0.9}'
return mock
# DESPUÉS: factory fixture + fixtures predefinidas para casos comunes
from tests.helpers import create_openai_chat_response
@pytest.fixture
def make_llm_response():
"""Factory: retorna la función para crear respuestas custom."""
return create_openai_chat_response
@pytest.fixture
def positive_response(make_llm_response):
"""Shortcut para el caso más común."""
return make_llm_response('{"sentiment": "positive", "confidence": 0.9}')
@pytest.fixture
def negative_response(make_llm_response):
return make_llm_response('{"sentiment": "negative", "confidence": 0.85}')
# Tests usan la fixture correcta para su caso:
# def test_positive_sentiment(positive_response): ← Conciso
# def test_custom_sentiment(make_llm_response): ← Flexible
# response = make_llm_response('{"sentiment": "neutral", "confidence": 0.1}')
Ejercicio 3: Escribir un regression test preventivo
Identifica una posible falla en la app de referencia y escribe un regression test para prevenirla (aunque el bug no haya ocurrido aún).
Ver guía
@pytest.mark.regression
def test_parser_preventive_handles_json_with_boolean_values():
"""
Preventivo: el parser debe manejar correctamente boolean values en JSON.
El LLM podría retornar 'true'/'false' (JSON) que son válidos pero
deben preservarse como bool en Python, no como string.
"""
raw = '{"is_spam": true, "confidence": 0.9}'
result = parse_json_from_llm_output(raw)
assert result["is_spam"] is True # bool, no string "true"
assert isinstance(result["is_spam"], bool)
@pytest.mark.regression
@patch("app.sentiment.client.chat.completions.create")
def test_analyze_sentiment_preventive_handles_null_confidence(mock_create, make_llm_response):
"""
Preventivo: si el LLM retorna 'null' para confidence, la función
debe manejarlo gracefully (no lanzar TypeError en la validación del rango).
"""
mock_create.return_value = make_llm_response(
'{"sentiment": "positive", "confidence": null}'
)
# Debe lanzar ValueError con mensaje claro, no TypeError
with pytest.raises((ValueError, TypeError)):
analyze_sentiment("texto")
Ejercicio 4: Plan de testing para el Módulo 2
Basándote en lo que aprendiste en el Módulo 1, ¿qué añadirías a tu suite de tests en el Módulo 2 que no puedes hacer aún?
Ver guía
Lo que NO puedes hacer con las herramientas del Módulo 1:
1. Snapshot testing
- Necesitas guardar el output "dorado" de una función
- Verificar que no cambia entre runs
- Herramienta: pytest-snapshot o similar
2. Testing de múltiples variaciones del mismo prompt
- Test matrix: prompt v1 vs v2 vs v3
- Detectar cuándo un cambio de prompt introduce regresiones
- Requiere estrategia más sofisticada de fixture factories
3. Testing exhaustivo de errores de la API
- RateLimitError, APITimeoutError, AuthenticationError
- Cada uno requiere un mock diferente con side_effect
- Necesitas entender la jerarquía de excepciones de openai
4. Testing de streaming
- Si tu app usa stream=True, el mock es muy diferente
- Requiere AsyncMock con generadores
5. Chain testing avanzado
- LangChain chains con múltiples pasos
- Cada paso del chain puede necesitar un mock diferente
Ejercicio 5: Auto-evaluación final
Responde honestamente: ¿cuántas de estas afirmaciones puedes confirmar?
[ ] Puedo explicar por qué los outputs no-determinísticos del LLM no son
una excusa para no testear.
[ ] Puedo configurar pytest.ini con markers, testpaths y addopts correctamente.
[ ] Puedo crear un conftest.py con una factory fixture para mocks del LLM.
[ ] Entiendo la diferencia entre smoke, contract, behavioral y regression tests.
[ ] Puedo escribir un contract test para un prompt que produce JSON.
[ ] Sé cómo usar @patch con el path correcto para interceptar llamadas a la API.
[ ] Puedo ejecutar pytest -m "not integration" y que tarde <30 segundos.
[ ] Sé diagnosticar por qué un test es flaky y cómo hacerlo robusto.
Si marcaste 7-8: estás listo para el Módulo 2. Si marcaste 5-6: repasa las cápsulas 03-05 antes de continuar. Si marcaste <5: completa el Proyecto 07 (Test Suite Setup) antes de avanzar.
Recursos adicionales
- pytest FAQ — Respuestas a preguntas frecuentes de configuración
- Where to patch — La guía más importante para entender
@patch - pytest-timeout — Detectar tests lentos automáticamente
- pytest --durations — Profiling de la suite
- Módulo 2: Unit Testing LLM Applications — Siguiente módulo
- Guía #12 Evaluation Frameworks — Complemento a testing para medir calidad de outputs