Construiste un agente de IA — un programa que usa un LLM (large language model — el cerebro detrás de ChatGPT, Claude, Gemini) para llamar herramientas de forma autónoma. Consulta bases de datos, verifica permisos, envía correos. En tu terminal, funciona siempre. Luego abres el directorio tests/ y te das cuenta: está vacío. No porque seas flojo, sino porque tu SDK (software development kit — la caja de herramientas que te da un framework) no incluyó absolutamente ninguna utilidad de testing.
Bienvenido al desarrollo de agentes en abril de 2026.
El vacío (en resumen)
Cubrimos el panorama de SDKs en detalle en la nota complementaria. La versión corta: Anthropic no incluye cero utilidades de testing, OpenAI se niega a exportar su FakeModel interno, y el evaluador de Google ADK tiene un bug abierto que devuelve 0.0 en coincidencias correctas. Nadie te está dando herramientas para testear tus agentes.
El siguiente pensamiento obvio: "Voy a hacer assert sobre la salida de texto del agente." No lo hagas. Un LLM reformula su respuesta en cada ejecución. El temperature (un parámetro que controla qué tan aleatoria es la salida del modelo) en cero no te salva — diferentes versiones del modelo, diferentes días, diferente redacción. Hacer matching de texto es testear un volado.
¿Qué no cambia? La secuencia de tool calls. Cuando tu agente maneja "verifica si el usuario puede enviar un correo", llama lookup_user → check_permissions → send_email siempre, sin importar cómo redacte la respuesta. El patrón de acciones es el contrato. Testea eso.
Esta guía te da un test harness funcional en ~60 líneas. Mocks, golden fixtures, separación de CI — todo lo que los SDKs deberían haber incluido pero no lo hicieron.
La receta: testing conductual en ~60 líneas
Vamos a construir un test harness — un mini framework que observa a tu agente — que:
- Mockea el LLM para que los tests corran en milisegundos a costo cero
- Registra qué herramientas llama el agente y en qué orden
- Hace assert sobre la secuencia, no sobre las palabras
Paso 1: Elige tu estrategia de mock
Dos caminos. Tests con mock-model reemplazan el LLM con un respondedor programado — instantáneo, gratis, determinístico. Smoke tests con modelo real llaman a la API real y graban respuestas — realistas pero caros e inestables. Necesitas ambos. Mocks en CI (cada pull request), modelo real en un cron nocturno.
Para mocking, Pydantic AI tiene las mejores primitivas: TestModel (auto-llama cada herramienta del agente) y FunctionModel (tú programas la respuesta exacta). Si no estás usando Pydantic AI, vamos a construir lo mismo desde cero.
Paso 2: Construye el interceptor
Aquí va un enfoque puro con pytest — sin dependencia de framework, funciona con cualquier SDK:
# test_agent_tools.py
import json
from dataclasses import dataclass, field
@dataclass
class ToolCallRecord:
"""Una invocación de herramienta registrada."""
name: str
arguments: dict
order: int
@dataclass
class AgentRecorder:
"""Intercepta y registra cada tool call."""
calls: list[ToolCallRecord] = field(default_factory=list)
def record(self, tool_name: str, arguments: dict):
self.calls.append(ToolCallRecord(
name=tool_name,
arguments=arguments,
order=len(self.calls),
))
@property
def sequence(self) -> list[str]:
"""Solo los nombres de herramientas, en orden."""
return [c.name for c in self.calls]
def assert_sequence(self, expected: list[str]):
assert self.sequence == expected, (
f"Expected {expected}, got {self.sequence}"
)
def assert_called_with(self, tool_name: str, **expected_args):
match = [c for c in self.calls if c.name == tool_name]
assert match, f"{tool_name} was never called"
assert match[0].arguments == expected_args
Son 35 líneas. El recorder captura cada invocación de herramienta y expone dos assertions: assert_sequence (¿herramientas correctas, orden correcto?) y assert_called_with (¿argumentos correctos?).
Paso 3: Conéctalo a tu agente
Envuelve tus funciones de herramienta para que el recorder vea cada llamada:
def make_tracked_tool(original_fn, recorder: AgentRecorder):
"""Envuelve una función de herramienta para registrar llamadas antes de ejecutar."""
async def tracked(*args, **kwargs):
recorder.record(original_fn.__name__, kwargs)
return await original_fn(*args, **kwargs)
return tracked
faion es un asistente CLI que genera scaffolding de código a partir de prompts estructurados — pega el bloque de abajo y generará el recorder y el wrapper adaptados a tu SDK.
/faion
Generate an AgentRecorder class and make_tracked_tool wrapper for behavioral testing of an AI agent.
Requirements:
- Pure Python, no framework dependency, dataclass-based
- AgentRecorder must record tool name, arguments, and call order
- Must expose assert_sequence(expected_names) and assert_called_with(tool_name, **kwargs)
- make_tracked_tool wraps any async tool function to record calls before executing
- Detect my agent SDK (anthropic / openai-agents / google-adk / pydantic-ai) from imports and adapt the wrapper signature accordingly
- Include a pytest fixture that returns a fresh AgentRecorder
Paso 4: Escribe tu primer test conductual
import pytest
@pytest.fixture
def recorder():
return AgentRecorder()
@pytest.mark.asyncio
async def test_email_permission_flow(recorder, mock_agent):
"""El agente DEBE verificar permisos antes de enviar correo."""
await mock_agent.run("Send the quarterly report to [email protected]")
# El CONTRATO: siempre verificar antes de enviar
recorder.assert_sequence([
"lookup_user",
"check_permissions",
"send_email",
])
# Verificar que el agente buscó al usuario correcto
recorder.assert_called_with(
"lookup_user", email="[email protected]"
)
Este test no le importa lo que el agente dice. Le importa lo que el agente hace. El texto puede cambiar en cada ejecución. La verificación de permisos no debe hacerlo.
Paso 5: Testea las rutas de recuperación de errores
La nota de noticias cubrió la tesis del "happy path". Las guías se ganan su lugar en las rutas feas — qué pasa cuando una herramienta falla, cuando el modelo reintenta, cuando alucina una herramienta que no existe.
@pytest.mark.asyncio
async def test_graceful_degradation_on_tool_failure(recorder, mock_agent):
"""Cuando send_email falla, el agente NO DEBE reintentar sin re-verificar permisos."""
mock_agent.configure_tool_failure("send_email", error="SMTP timeout")
await mock_agent.run("Send the quarterly report to [email protected]")
recorder.assert_sequence([
"lookup_user",
"check_permissions",
"send_email", # falla
"check_permissions", # debe re-verificar antes de reintentar
"send_email", # reintento
])
@pytest.mark.asyncio
async def test_unknown_tool_call_is_caught(recorder, mock_agent):
"""Si el modelo alucina un nombre de herramienta, el harness lo atrapa."""
mock_agent.inject_fake_tool_call("send_fax") # no existe
with pytest.raises(UnknownToolError):
await mock_agent.run("Fax the report")
# El agente no debería haber llamado nada exitosamente
assert recorder.sequence == []
La recuperación de errores es donde los agentes se rompen silenciosamente en producción. Un mock que simula fallas de herramientas no cuesta nada y atrapa las fallas que tus tests de happy-path jamás van a encontrar.
/faion
Scaffold a full behavioral test suite for my AI agent project.
Requirements:
- Scan my agent's registered tools and generate one test per tool
- For each tool: happy-path test + error-recovery test (tool raises exception)
- Generate a mock agent fixture that uses scripted responses instead of real LLM calls
- Include conftest.py with AgentRecorder fixture and make_tracked_tool helper
- Add golden fixture save/load utilities (JSON snapshots in tests/fixtures/)
- Mark real-model tests with @pytest.mark.smoke
- Detect my SDK from imports and adapt accordingly
Paso 6: Agrega golden fixtures para detectar regresiones
Un golden fixture es un snapshot del comportamiento esperado que guardas en control de versiones — tu línea base bendecida. Guarda la salida de tu recorder como JSON:
# conftest.py
import json
from pathlib import Path
FIXTURE_DIR = Path("tests/fixtures")
def save_golden(recorder: AgentRecorder, name: str):
"""Guarda la secuencia actual de tool-calls como la línea base bendecida."""
fixture = {
"sequence": recorder.sequence,
"calls": [
{"name": c.name, "arguments": c.arguments}
for c in recorder.calls
],
}
(FIXTURE_DIR / f"{name}.json").write_text(json.dumps(fixture, indent=2))
def assert_matches_golden(recorder: AgentRecorder, name: str):
"""Verifica que el comportamiento actual coincida con la línea base guardada."""
fixture = json.loads((FIXTURE_DIR / f"{name}.json").read_text())
assert recorder.sequence == fixture["sequence"], (
f"Behavioral drift detected!\n"
f" Golden: {fixture['sequence']}\n"
f" Actual: {recorder.sequence}"
)
Cuando actualices tu modelo de Claude Sonnet 4 a lo que sea que salga después, corre la suite. Si el agente empieza a saltarse check_permissions, lo vas a saber antes que tus usuarios.
Paso 7: Integración con CI
Tu configuración de CI se divide en dos jobs. La pirámide de testing de Block Engineering clava el principio: "No corremos tests con LLM en vivo en CI. Es demasiado caro, demasiado lento y demasiado inestable."
# .github/workflows/agent-tests.yml
name: Agent Tests
on: [pull_request]
jobs:
unit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: pytest tests/agents/ -m "not smoke" --tb=short
# .github/workflows/agent-smoke.yml
name: Agent Smoke Tests (Nightly)
on:
schedule:
- cron: '0 6 * * *' # 2am ET
jobs:
smoke:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: pytest tests/agents/ -m smoke --tb=long
Marca los tests con modelo real con @pytest.mark.smoke. Corren de noche, alertan sobre drift, jamás bloquean un PR.
/faion
Generate GitHub Actions CI config for agent behavioral tests.
Requirements:
- Two workflow files: agent-tests.yml (on PR) and agent-smoke.yml (nightly cron at 2am ET)
- PR workflow runs pytest with -m "not smoke", fails fast
- Nightly workflow runs pytest with -m smoke, uses secrets for API keys
- Add a third workflow: agent-golden-update.yml (manual trigger) that runs tests with --update-golden flag and opens a PR with fixture diffs
- Include pip caching and Python 3.12 setup
Trampas
1. Anthropic dice que no hagas esto. Su blog de ingeniería (9 de enero de 2026) advierte explícitamente: "Verificar que los agentes siguieron pasos muy específicos como una secuencia de tool calls resulta en tests excesivamente frágiles." Tienen razón — para flujos exploratorios de múltiples pasos donde existen varias rutas válidas. Se equivocan para rutas críticas de negocio donde saltarse un paso significa un agujero de seguridad. Usa assertions de secuencia para contratos (verificación de permisos, validación de datos), assertions de resultado para todo lo demás.
2. Los golden fixtures se vuelven obsoletos. Actualizas tu modelo y cada fixture se rompe — no porque el agente esté mal, sino porque encontró una ruta legítimamente mejor. Solución: revisa los diffs como code reviews. Si la nueva secuencia es válida, actualiza el fixture. Si no lo es, acabas de atrapar una regresión.
3. Los tests con mock te mienten. Un FakeModel que siempre llama check_permissions te dice que tu lógica de ruteo funciona. No te dice nada sobre si Claude o GPT realmente van a invocar esa herramienta. Los tests con mock cubren aproximadamente el 70% de la superficie testeable — ruteo, parseo de argumentos, manejo de errores. El 30% restante necesita smoke tests con modelo real en un schedule nocturno.
4. El no-determinismo es estructural, no un bug. Incluso con la misma entrada, un modelo podría llamar herramientas en diferente orden entre ejecuciones. Para rutas no críticas, usa AgentEvals con modo de trayectoria unordered — verifica el conjunto de herramientas llamadas, no la secuencia. Reserva el orden estricto para rutas donde el orden ES el contrato.
5. Testea los handoffs multi-agente por separado. Si tu agente delega a sub-agentes, cada handoff es una frontera. Registra la llamada de delegación como un tool call (delegate_to_research_agent), luego testea la secuencia de herramientas del sub-agente de forma independiente. Cruzar fronteras de agentes en una sola assertion hace que todo sea frágil y nada sea debuggeable.
Lo que puedes hacer ahora
Tienes un directorio tests/agents/ con tests conductuales, golden fixtures en control de versiones, y una separación de CI que corre mocks rápidos en cada PR y smoke tests con modelo real por las noches. Tu agente se despliega con la misma confianza que el resto de tu código — no porque se volvió determinístico, sino porque dejaste de testear lo que no debías. El texto cambia en cada ejecución. Las acciones no deberían.





