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ápsulaConcepto clavePara recordar
01Intro y setupTesting verifica código; Evaluation mide calidad del output LLM
02Por qué diferente70-80% del código es determinístico. Non-determinism no es excusa
03Configuraciónpytest.ini + conftest.py + markers + parametrize. Scope de fixtures
04Anatomía AAAArrange incluye mock. Una Act por test. Assert con mensajes informativos
05FixturesMagicMock (no dicts). Factory fixtures. AsyncMock para código async
06TaxonomíaSmoke → Contract → Behavioral → Regression. Cada uno tiene su propósito
07ProyectoTest Suite Setup completo con todos los tipos de tests
08Este móduloTroubleshooting + 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:

  1. Fix flaky tests (más impacto en confianza de la suite)
  2. Fix tests lentos (más impacto en velocidad de desarrollo)
  3. 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

  1. pytest FAQ — Respuestas a preguntas frecuentes de configuración
  2. Where to patch — La guía más importante para entender @patch
  3. pytest-timeout — Detectar tests lentos automáticamente
  4. pytest --durations — Profiling de la suite
  5. Módulo 2: Unit Testing LLM Applications — Siguiente módulo
  6. Guía #12 Evaluation Frameworks — Complemento a testing para medir calidad de outputs