Fixtures & Mocks
Dateien, dynamische Fixtures, die Bereinigung externer Zustände und gezielte Mocks für eigene Tools und Lifecycle-Phasen machen Agenten-Tests reproduzierbar.
Auf dieser Seite
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:
- 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, mitapplication/octet-streamals 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.
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 Nonetests:
- 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 == 1Testdaten 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.
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:
- 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 == 1Einige Hinweise:
- Leerer Kontext bei Tests ohne Builder. Tests ohne
buildererhaltencontext = {}. 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 inexpected_resultgenauso wie innerhalb einerparams- oderinvocations-Bedingung.
Rückgabewerte von cleanup()
TrueoderNonezurückgeben, damit der Testfall besteht. Verwende diese Rückgabewerte, wenn die Funktion nur aufräumt und die Agentenantwort nicht bewertet.Falsezurü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_callsundexpected_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.
buildundcleanupdü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
builderals auchfilesgesetzt, werden angehängte Fixtures mit der Builder-Ausgabe zusammengeführt. Gibt der Builder ein Dictionary mit eigenemfiles-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:
| Funktionsname | Ersetzt |
|---|---|
| mock_data_customer_add_customer | Genau die Funktion add_customer in tools/data/customer.py |
| mock_data_customer | Jedes Tool in tools/data/customer.py |
| mock_data | Jedes Tool unter tools/data/ |
| mock | Jedes 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_before → mock_data_customer_hook_before → mock_data_hook_before → mock_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_check → guardrail_input → guardrail. 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.
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:
- 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_nameist 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, …) undapi:Tool-Implementierungen werden real ausgeführt. Aufrufe ausgehender Verbindungen aus Agenten-Tools sowie übersend_connectorausgefü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_mockslä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_mocksgilt nur für Tools. Setze es im Testfall oder indefaults, 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_mocksundstrict_guardrail_mocksverwenden jeweilsfalseals Standardwert und können indefaultsoder 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.