Ir al contenido principal
Dependency injection en agentes IA: tests sin mocks hardcodeados

Dependency injection en agentes IA: tests sin mocks hardcodeados

AI Integration
5 min readPor Daily Miranda Pardo

Hay un bug en producción. El agente de facturación generó 23 facturas con el cliente equivocado. El CTO llama. Abres el entorno local, ejecutas el mismo caso y funciona perfectamente.

No puedes reproducirlo porque las herramientas del agente están hardcodeadas: la base de datos real, el servicio de email real, la API externa real. En local no tienes los mismos datos. En CI tampoco. El bug existe en un entorno que no controlas.

Este es el coste exacto de construir agentes IA sin dependency injection.

Por qué los agentes sin DI son imposibles de testear

Un agente IA típico tiene herramientas: consulta la base de datos, envía emails, llama a APIs externas. Si esas herramientas se instancian dentro del agente, tienes tres problemas que se fusionan en uno grande:

Problema 1: Tests que rompen cosas reales. Cada vez que corres el test suite, el agente envía emails de verdad, modifica datos en producción y consume créditos de API. O comentas los tests, o te arriesgas a efectos secundarios.

Problema 2: No puedes reproducir bugs de producción. En producción el agente ve datos reales con estados específicos. En local ves otra BD con otros datos. El bug existe en un contexto que no controlas ni puedes replicar.

Problema 3: No puedes probar edge cases. ¿Qué hace el agente cuando la BD devuelve un timeout? ¿Cuando el servicio de email está caído? Con herramientas hardcodeadas, provocar eso en tests sin afectar producción es prácticamente imposible.

La solución lleva décadas en el desarrollo de software clásico: dependency injection. Pasas las dependencias desde fuera, en lugar de crearlas dentro.

El patrón ToolContainer

Define una interfaz TypeScript que agrupa todas las herramientas del agente. La implementación puede ser real, de staging o un mock:

// tools/types.ts
export interface AgentToolContainer {
  db: {
    getCustomer: (id: string) => Promise<Customer | null>;
    saveInvoice: (invoice: Invoice) => Promise<string>;
  };
  email: {
    send: (to: string, template: string, data: object) => Promise<void>;
  };
  search: {
    query: (q: string) => Promise<SearchResult[]>;
  };
}

Luego defines implementaciones distintas por entorno:

// tools/environments.ts
export const productionTools: AgentToolContainer = {
  db: new SupabaseDBTool(supabaseClient),
  email: new ResendEmailTool(resendApiKey),
  search: new TavilySearchTool(tavilyApiKey),
};

export const stagingTools: AgentToolContainer = {
  db: new SupabaseDBTool(stagingSupabase),
  email: new MockEmailTool({ verbose: true }),   // no envía, solo loguea
  search: new CachedSearchTool(tavilyApiKey),    // respuestas cacheadas
};

export const testTools = (overrides: Partial<AgentToolContainer>) =>
  createMockToolContainer(overrides);

El agente que no sabe qué entorno usa

El agente recibe el container como parámetro. No construye nada internamente:

// agents/billing-agent.ts
export async function runBillingAgent(
  userMessage: string,
  tools: AgentToolContainer,
): Promise<AgentResult> {
  const anthropicTools = buildToolDefinitions(tools);

  const response = await anthropic.messages.create({
    model: 'claude-opus-4-7',
    tools: anthropicTools,
    max_tokens: 2048,
    messages: [{ role: 'user', content: userMessage }],
  });

  return processResponse(response, tools);
}

En el endpoint de Next.js usas productionTools. En los tests, mocks. El agente no cambia ni una línea:

// app/api/billing/route.ts
export async function POST(req: Request) {
  const { message } = await req.json();
  return runBillingAgent(message, productionTools);
}

Tests deterministas en milisegundos

Con el ToolContainer inyectado, los tests son limpios, instantáneos y sin efectos secundarios:

// __tests__/billing-agent.test.ts
describe('BillingAgent', () => {
  it('genera factura cuando el cliente existe', async () => {
    const saveInvoice = jest.fn().mockResolvedValue('INV-001');
    const tools = testTools({
      db: {
        getCustomer: () => Promise.resolve(fixtures.customer123),
        saveInvoice,
      },
    });

    const result = await runBillingAgent(
      'Genera factura para el cliente 123',
      tools,
    );

    expect(saveInvoice).toHaveBeenCalledWith(
      expect.objectContaining({ customerId: '123', status: 'draft' }),
    );
    expect(result.status).toBe('success');
  });

  it('maneja error de BD con respuesta controlada', async () => {
    const tools = testTools({
      db: {
        getCustomer: () => Promise.reject(new Error('timeout')),
        saveInvoice: jest.fn(),
      },
    });

    const result = await runBillingAgent(
      'Genera factura para el cliente 123',
      tools,
    );

    expect(result.status).toBe('error');
    expect(result.message).toContain('no disponible');
  });
});

El segundo test prueba el comportamiento del agente cuando la BD falla. Con herramientas hardcodeadas esto requiere infraestructura de mocking mucho más compleja. Aquí son 5 líneas.

Fixture recording: casos reales como fixtures de test

El nivel siguiente es grabar las respuestas reales de las herramientas en producción y convertirlas en fixtures:

// tools/recording-tool.ts
export class RecordingDBTool implements AgentToolContainer['db'] {
  constructor(
    private real: AgentToolContainer['db'],
    private recorder: FixtureRecorder,
  ) {}

  async getCustomer(id: string) {
    const result = await this.real.getCustomer(id);
    await this.recorder.save(`db.getCustomer.${id}`, result);
    return result;
  }

  async saveInvoice(invoice: Invoice) {
    const result = await this.real.saveInvoice(invoice);
    await this.recorder.save('db.saveInvoice', { input: invoice, output: result });
    return result;
  }
}

Activas RecordingDBTool en producción 30 minutos, guardas los fixtures en un JSON y ya tienes casos reales para tus tests. Es el equivalente a VCR cassettes para HTTP, aplicado a herramientas de agentes IA. Cuando llegue el bug de producción que no puedes reproducir, el fixture ya estará grabado.

Qué cambia cuando aplicas este patrón

Un equipo que implementa esto pasa de:

  • CI que tarda 8 minutos y falla 3 de cada 10 veces por timeouts de servicios externos
  • Tests que nadie ejecuta porque "rompen datos en staging"
  • Bugs de producción que tardan horas en reproducirse

A:

  • Suite de tests que corre en menos de 30 segundos sin tocar ningún servicio externo
  • 15 edge cases cubiertos con datos completamente controlados
  • Reproducción de cualquier bug de producción en local en menos de 5 minutos

Este patrón es el primer cambio estructural que recomiendo cuando un equipo tiene deuda técnica en su capa de integración IA: antes de añadir más herramientas o más agentes, primero haz que los existentes sean testeables.


Si tu equipo está construyendo agentes y los tests son el principal freno para iterar con confianza, hablamos directamente:

Cuéntame cómo está montado tu sistema →

Compartir artículo

LinkedInXWhatsApp

¿Procesos repetitivos en tu empresa?

Descarga gratis el Mapa de Automatización IA — los 5 procesos que más tiempo roban y cómo resolverlos.

Sin spam. Solo el PDF. Puedes darte de baja cuando quieras.

Escrito por Daily Miranda Pardo

Ayudo a empresas a automatizar procesos, crear agentes IA y conectar sistemas inteligentes.