Zum Hauptinhalt springen
Connic
Test

Fixtures & Mocks

Dateien, dynamische Fixtures, die Bereinigung externer Zustände und gezielte Mocks für eigene Tools und Lifecycle-Phasen machen Agenten-Tests reproduzierbar.

Zuletzt aktualisiert

Dateianhänge

Lege Input-Dateien für Agenten wie PDFs, Bilder und Audio in tests/files/ ab und referenziere sie in der Liste files: des Testfalls nur über den Dateinamen. Jede Datei wird Base64-codiert und unter files angehängt. Ist payload ein JSON-Objekt oder gibt ein builder ein Dict zurück, bleiben dessen Keys neben files auf oberster Ebene. Andere Payloads werden als {message: payload} übergeben.

tests/invoice-agent.yaml
tests:
  - name: extracts_invoice_total
    payload: "extrahiere den Gesamtbetrag als JSON"
    files:
      - invoice_acme.pdf
      - invoice_globex.pdf
    expected_result: output.total > 0

  # Dateien werden mit einer statischen Payload (dem Prompt) kombiniert.
  # Auch die Verwendung mit einem Builder ist möglich. Angehängte Dateien
  # werden mit dem Return Value des Builders zusammengeführt.
  - name: classifies_receipt
    payload: "ist das eine Essens- oder Reiseausgabe?"
    files:
      - receipt.jpg
    expected_result: 'output.category in ("meal", "travel")'

Wichtige Einschränkungen:

  • Nur reine Dateinamen. Keine Pfadtrenner und kein ... Das Schema lehnt alles ab, was wie ein Pfad aussieht.
  • 25 MB Upload-Limit. Code und Konfiguration, also alles außerhalb von tests/files/, sind weiterhin auf 5 MB begrenzt; Fixtures erhalten den verbleibenden Spielraum.
  • Der MIME-Typ wird automatisch erkannt: anhand der Dateiendung über Pythons mimetypes, mit application/octet-stream als Fallback.
  • Fehlende Dateien stoppen die Testsuite. Jeder Dateiname in files: muss vor Beginn der Agent-Ausführung existieren.

Dynamische Payload Builder

Verwende einen Builder, wenn die Agenteneingabe von einer Fixture, Datenbankzeile oder generierten ID abhängt. Lege unter tests/builders/ ein Python-Modul mit build(context, builder_args, test_name, payload, files) und optional cleanup(run, context, builder_args) an und referenziere es mit builder:. Diese Funktionen laufen einmal pro Aufruf. Der String oder das Dictionary, das build() zurückgibt, wird zur Agenteneingabe und ersetzt jede statische payload.

tests/builders/create_charge_then_refund.py
import os, requests

def build(context, builder_args, test_name, payload, files):
    """Fixture in der eigenen API bereitstellen und anschließend die Agent Payload zurückgeben.

    context       dict       -- verändern, um State an cleanup() UND an
                                expected_result / expected_tool_calls weiterzugeben
    builder_args  dict       -- die YAML `builder_args`
    test_name     str        -- der YAML-Wert `name`
    payload       str | None -- die YAML-`payload` (falls vorhanden)
    files         list[str]  -- die YAML-`files`
    """
    charge = requests.post(
        f"{os.environ['BILLING_API']}/charges",
        json={"amount_cents": builder_args["amount_cents"], "currency": "eur"},
    ).json()
    # ID speichern, damit cleanup() die Fixture entfernen UND die
    # YAML Expressions über `context.charge_id` darauf zugreifen können.
    context["charge_id"] = charge["id"]
    return {"charge_id": charge["id"], "instruction": "erstatte diese Zahlung"}


def cleanup(run, context, builder_args):
    """Fixture entfernen. Optional Checks auf Python-Ebene ergänzen.

    Läuft nach jedem Agent Call, unabhängig von Erfolg, Fehler oder Timeout,
    damit externe Ressourcen immer freigegeben werden.

    run["input"]    -- was der Agent gesehen hat
    run["output"]   -- der geparste Output des Agenten
    run["context"]  -- das run_context Dict des Runs (run_id, agent_name,
                       connector_id, timestamp sowie alles, was Middleware
                       oder Hooks während des Runs ergänzt haben)
    context         -- das in build() befüllte Dict
    builder_args    -- dasselbe Dict, das an build() übergeben wurde
    """
    requests.delete(f"{os.environ['BILLING_API']}/charges/{context['charge_id']}")
    # False zurückgeben, damit der Case zusätzlich zu den YAML Checks fehlschlägt;
    # True/None zum Bestehen zurückgeben.
    return run["output"].get("refund_id") is not None
tests/billing-agent.yaml
tests:
  - name: refunds_a_real_charge
    builder: create_charge_then_refund
    builder_args:
      amount_cents: 4200
    # `context` ist dasselbe Dict, das build() verändert hat. Hier bindet es
    # den Tool Call an genau die vom Builder bereitgestellte charge_id. Der Test
    # schlägt fehl, wenn der Agent eine ID erfindet oder die falsche Zahlung erstattet.
    expected_result: output.status == "refunded" and output.charge_id == context.charge_id
    expected_tool_calls:
      - billing.refund: params.charge_id == context.charge_id and invocations == 1

Testdaten von build an cleanup übergeben

Speichere benötigte Werte im Dictionary context, während build() die Testdaten erstellt. Dasselbe Dictionary wird als zweites Argument an cleanup() übergeben. So kann der Builder beispielsweise die ID eines angelegten Datensatzes speichern und cleanup diesen Datensatz später per DELETE löschen.

Builder-Daten in YAML-Assertions prüfen

Das Dictionary context ist unter demselben Namen context auch in den Ausdrücken für expected_result und expected_tool_calls verfügbar. Der Builder erzeugt eine ID und übergibt sie dem Agenten in der Payload. Die Assertion prüft anschließend, ob der Agent genau diese ID an das Tool weitergegeben hat. Eine erfundene ID oder die ID eines anderen Datensatzes lässt den Test fehlschlagen.

tests/builders/insert_then_query.py
import os, uuid, requests

def build(context, builder_args, test_name, payload, files):
    test_uuid = str(uuid.uuid4())
    requests.post(
        f"{os.environ['DB_API']}/rows",
        json={"id": test_uuid, "value": "hello"},
    )
    context["test_uuid"] = test_uuid
    return f"Rufe die Zeile mit der ID {test_uuid} ab und nenne mir ihren Wert."

def cleanup(run, context, builder_args):
    requests.delete(f"{os.environ['DB_API']}/rows/{context['test_uuid']}")
tests/db-agent.yaml
tests:
  - name: fetches_the_row_we_just_inserted
    builder: insert_then_query
    expected_result: output.row.id == context.test_uuid
    expected_tool_calls:
      - db.fetch_row: params.uuid == context.test_uuid and invocations == 1

Einige Hinweise:

  • Leerer Kontext bei Tests ohne Builder. Tests ohne builder erhalten context = {}. Bei einem fehlenden Schlüssel ist die Bedingung nicht erfüllt; es wird keine Exception ausgelöst.
  • Nach build und vor cleanup lesen. Assertions prüfen das Dictionary in dem Zustand, in dem build() zurückkehrt. cleanup() erhält dasselbe Dictionary und kann es beim Aufräumen verändern. Auf die Assertions hat das keinen Einfluss, da sie vorher ausgewertet werden.
  • Auf beiden Seiten dieselbe Syntax. context.foo.bar[0] funktioniert in expected_result genauso wie innerhalb einer params- oder invocations-Bedingung.

Rückgabewerte von cleanup()

  • True oder None zurückgeben, damit der Testfall besteht. Verwende diese Rückgabewerte, wenn die Funktion nur aufräumt und die Agentenantwort nicht bewertet.
  • False zurückgeben, damit der Testfall fehlschlägt. Das ist für Assertions auf Python-Ebene nützlich, die sich nicht als YAML-Ausdruck ausdrücken lassen. Zusätzlich müssen alle in YAML definierten Prüfungen bestehen: expected_result, expected_tool_calls und expected_no_tool_calls.
  • Läuft immer. Die Bereinigung läuft auch nach Timeouts und Fehlern des Agenten. Eine Exception in der Cleanup-Funktion lässt den Testfall fehlschlagen und erscheint in failure_reason.

Weitere Hinweise

  • Zustand wird pro Aufruf zurückgesetzt. Caches und Zähler auf Modulebene bleiben nicht zwischen Builder-Aufrufen erhalten.
  • Sync oder Async. build und cleanup dürfen einen Wert oder eine Coroutine zurückgeben.
  • Ausgewählte Testumgebung. Für Builder gelten dieselben Umgebungsvariablen und Netzwerkzugriffe wie für den Agentenaufruf.
  • Mit Dateien kombinieren. Sind sowohl builder als auch files gesetzt, werden angehängte Fixtures mit der Builder-Ausgabe zusammengeführt. Gibt der Builder ein Dictionary mit eigenem files-Schlüssel zurück, werden beide Listen zusammengeführt.
  • Fehlende Builder stoppen die Testsuite. Ein referenziertes Builder-Modul muss vor Beginn der Agent-Ausführung existieren.

Tools und Lifecycle-Code mocken

Tests führen konfigurierte eigene Tools, Middleware, Hooks und Guardrails aus, sofern ein Testfall sie nicht ersetzt. Automatische ausgehende Verbindungen werden unterdrückt. Aufrufe ausgehender Verbindungen aus Agenten-Tools und Middleware werden für Assertions aufgezeichnet, aber nie zugestellt. Um weitere Seiteneffekte zu isolieren, verweise mit mocks: auf ein Python-Modul unter tests/mocks/. Dieses Modul kann Ergebnisse von eigenen dateibasierten Tools und einzelne Lifecycle-Phasen ersetzen.

Für jede Funktion lässt sich ein eigener Mock definieren. Ist kein passender Mock vorhanden, läuft der konfigurierte Projektcode, sofern das entsprechende Strict-Mock-Flag keinen Ersatz verlangt.

Tool-Ergebnisse

Für eine Tool-Referenz wie data.customer.add_customer wird der erste definierte Name in dieser Reihenfolge verwendet:

FunktionsnameErsetzt
mock_data_customer_add_customerGenau die Funktion add_customer in tools/data/customer.py
mock_data_customerJedes Tool in tools/data/customer.py
mock_dataJedes Tool unter tools/data/
mockJedes eigene dateibasierte Tool, für das kein spezifischerer Mock definiert ist

Middleware-Phasen

Definiere middleware_before(content, context), um die before Middleware des Agenten zu ersetzen, oder middleware_after(response, context) für after. Gib denselben Wert zurück wie die echte Phase.

Tool-Hook-Phasen

Hook-Ersetzungen verwenden die Tool-Hierarchie plus Phasensuffix. Für dasselbe Tool löst before in dieser Reihenfolge auf: mock_data_customer_add_customer_hook_beforemock_data_customer_hook_beforemock_data_hook_beforemock_hook_before. Die after Kette verwendet dieselben Namen mit der Endung _hook_after. Nicht alphanumerische Folgen in jedem Segment der Tool-Referenz werden zu _ normalisiert. Deshalb löst api:weather-v2.lookup zu mock_api_weather_v2_lookup_hook_before auf. Before-Handler verwenden (tool_name, params, context) → params; After-Handler verwenden (tool_name, params, result, context) → result.

Eigene Guardrails

Für einen eigenen Input-Guardrail namens domain_check erfolgt die Auflösung als guardrail_input_domain_checkguardrail_inputguardrail. Output-Guardrails verwenden die entsprechende Reihenfolge guardrail_output_*. Jeder Handler verwendet (content, context) → GuardrailResult. Namen werden für Python normalisiert, deshalb löst auch domain-check zu domain_check auf. Nur Guardrails mit type: custom sind zulässig; integrierte Guardrails werden nicht ersetzt.

tests/mocks/customer_mocks.py
from connic import GuardrailResult

# Ersetzungen für Tool Results verwenden hierarchische mock_*-Namen.
def mock_data_customer_add_customer(tool_name, params, context):
    return {"id": "cust_test_1", "name": params.get("name")}

# Middleware-Ersetzungen verwenden die echten Phase Signatures.
def middleware_before(content, context):
    context["customer_id"] = "cust_test_1"
    return content

def middleware_after(response, context):
    return response

# Hook-Ersetzungen verwenden die Tool-Hierarchie plus Phase Suffix.
def mock_data_customer_add_customer_hook_before(tool_name, params, context):
    params["name"] = params["name"].strip()
    return params

def mock_hook_after(tool_name, params, result, context):
    return result

# Nur Guardrails mit type: custom können ersetzt werden.
def guardrail_input_domain_check(content, context):
    return GuardrailResult(passed=True)
tests/customer-agent.yaml
tests:
  - name: adds_a_customer_without_touching_the_db
    payload: '{"name": "Ada"}'
    mocks: customer_mocks
    strict_mocks: true
    strict_hook_mocks: true
    strict_middleware_mocks: true
    strict_guardrail_mocks: true
    # Der add_customer Call des Agenten wird vom Mock bedient, aber weiterhin
    # aufgezeichnet. So lässt sich prüfen, ob der Agent ihn mit dem richtigen
    # Argument aufgerufen hat, ohne je in einen echten Datastore zu schreiben.
    expected_tool_calls:
      - data.customer.add_customer: params.name == "Ada"
    expected_result: output.id == "cust_test_1"
  • Schnittstelle der Tool-Mocks. Tool Mocks verwenden (tool_name, params, context) → result. tool_name ist die vollständige Referenz, sodass ein Mock für mehrere Tools anhand des Namens entscheiden kann, welches Ergebnis er zurückgibt.
  • Welche Tools sich ersetzen lassen. Nur Implementierungen von eigenen dateibasierten Tools lassen sich ersetzen. Vordefinierte Tools (db_find, web_search, trigger_agent, …) und api: Tool-Implementierungen werden real ausgeführt. Aufrufe ausgehender Verbindungen aus Agenten-Tools sowie über send_connector ausgeführte Middleware-Aufrufe sind Ausnahmen: Tests zeichnen sie als gemockt auf und überspringen die Zustellung.
  • Nur Ersatz. Ein Lifecycle-Mock ersetzt eine vorhandene Middleware-, Hook- oder konfigurierte Custom-Guardrail-Phase. Er ergänzt keine Phase, die das Projekt nicht definiert. Custom-Guardrail-Dateien müssen weiterhin erfolgreich geladen werden.
  • Hook-Ersetzungen sind unabhängig. Wo Agenten-Hooks regulär laufen, kann eine Hook-Phase unabhängig davon ersetzt werden, ob das Tool-Ergebnis echt oder gemockt ist. Ohne passende Hook-Ersetzung läuft standardmäßig der echte Hook; strict_hook_mocks lässt den Testfall stattdessen vor dessen Ausführung fehlschlagen. Entfernte MCP-Tools führen keine Agenten-Hooks aus, daher gelten Hook-Ersetzungen für sie nicht.
  • Parameterprüfung. Auch bei gemockten Aufrufen wird geprüft, ob alle Pflichtargumente vorhanden sind und Namen sowie Datentypen stimmen. Ungültige Argumente lassen den Testfall fehlschlagen. Parameter mit Standardwert sind optional.
  • Tracing und Assertions. Ein gemockter Aufruf erscheint im Trace mit einem mocked-Badge und zählt für expected_tool_calls / expected_no_tool_calls.
  • strict_mocks gilt nur für Tools. Setze es im Testfall oder in defaults, damit der Testfall vor der Ausführung eines nicht gemockten eigenen dateibasierten Tools fehlschlägt. Es steuert keine Middleware-, Hook- oder Guardrail-Ersetzungen.
  • Mocks für jede Phase separat vorschreiben. strict_hook_mocks, strict_middleware_mocks und strict_guardrail_mocks verwenden jeweils false als Standardwert und können in defaults oder pro Testfall gesetzt werden. Ist eines dieser Flags aktiviert, schlägt der Testfall vor der Ausführung der zugehörigen Phase fehl, wenn kein passender Mock vorhanden ist. Fehlende Hook-, Middleware- oder Guardrail-Phasen sowie integrierte Guardrails sind ausgenommen.
  • Verwendet die tatsächliche Kontext-Schnittstelle. Middleware-, Hook- und Tool-Ersetzungen erhalten den geteilten Run-Kontext; Custom-Guardrail-Ersetzungen erhalten dieselbe Guardrail-Kontextansicht wie der echte Check. Der Builder-Kontext ist separat und wird von Assertions, Freigabeentscheidungen und Cleanup verwendet.
  • Zustand wird pro Aufruf zurückgesetzt. Zähler und Caches auf Modulebene bleiben nicht zwischen Mock-Aufrufen erhalten.
  • Fehlende Module stoppen die Testsuite. Ein über mocks: referenziertes Modul muss vor Beginn der Agent-Ausführung existieren.