Testing Google ADK TypeScript Agents Without Chasing Sentences

Testing Google ADK TypeScript Agents Without Chasing Sentences

The fastest way to make an AI-agent test flaky is to assert the final sentence. You expect: I'll help you find hotels in Paris. Enter fullscreen mode Exit fullscreen mode The agent returns: Sure — I can look for hotel options in Paris. Enter fullscreen mode Exit fullscreen mode The behavior is correct, but the test is red. Google's Agent Development Kit (ADK) brings agents closer to conventional software engineering: agents, tools, orchestration, sessions, events, evaluation, and deployment are represented as code and runtime primitives. That does not make the model deterministic. It gives us better places to establish deterministic contracts around it. Do not test the agent's personality. Test its decisions and boundaries. Start with a testing pyramid for agents A useful agent test suite has four layers: ┌──────────────────────────┐ │ Small human-reviewed evals│ ┌──┴──────────────────────────┴──┐ │ End-to-end trajectory scenarios │ ┌──┴──────────────────────────────────┴──┐ │ Runtime contracts: policy, state, schema │ ┌──┴──────────────────────────────────────────┴──┐ │ Deterministic unit tests for tools and adapters │ └─────────────────────────────────────────────────┘ Enter fullscreen mode Exit fullscreen mode Most tests should live near the bottom. They are fast, cheap, and deterministic. Use live-model evaluations deliberately, not for every assertion. 1. Unit-test tools without a model ADK TypeScript tools can be expressed with FunctionTool and a Zod parameter schema. The business function underneath is still ordinary TypeScript and should be tested that way. import { FunctionTool } from "@google/adk"; import { z } from "zod"; export const searchHotels = async ({ city, maxNightlyPriceUsd, }: { city: string; maxNightlyPriceUsd?: number; }) => { return hotelGateway.search({ city, maxNightlyPriceUsd }); }; export const searchHotelsTool = new FunctionTool({ name: "search_hotels", description: "Search available hotels. This tool never creates a booking.", parameters: z.object({ city: z.string().min(2), maxNightlyPriceUsd: z.number().positive().optional(), }), execute: searchHotels, }); Enter fullscreen mode Exit fullscreen mode The first test should not involve Gemini or ADK's event loop: import { describe, expect, it, vi } from "vitest"; it("passes normalized filters to the hotel gateway", async () => { vi.spyOn(hotelGateway, "search").mockResolvedValue([]); await searchHotels({ city: "Paris", maxNightlyPriceUsd: 250, }); expect(hotelGateway.search).toHaveBeenCalledWith({ city: "Paris", maxNightlyPriceUsd: 250, }); }); Enter fullscreen mode Exit fullscreen mode Tool permissions, data mapping, error normalization, and idempotency do not become probabilistic merely because a model selected the tool. 2. Assert the trajectory, not the prose For an integration test, run the ADK agent and collect its events. Convert framework events into a small application-owned summary so your tests are not coupled to every internal event detail. import { InMemoryRunner, LlmAgent } from "@google/adk"; const agent = new LlmAgent({ name: "travel_assistant", model: "gemini-2.5-flash", instruction: [ "Use search_hotels for availability questions.", "Never call book_hotel without explicit confirmation.", "Ask a clarification question when the city is missing.", ].join("\n"), tools: [searchHotelsTool, bookHotelTool], }); async function runScenario(input: string) { const runner = new InMemoryRunner({ agent }); const session = await runner.sessionService.createSession({ appName: runner.appName, userId: "test-user", }); const events = []; for await (const event of runner.runAsync({ userId: session.userId, sessionId: session.id, newMessage: { role: "user", parts: [{ text: input }], }, })) { events.push(event); } return summarizeTrajectory(events); } Enter fullscreen mode Exit fullscreen mode summarizeTrajectory is deliberately your adapter. It can return a stable contract such as: type TrajectorySummary = { toolCalls: Array; blockedActions: string[]; clarificationRequested: boolean; finalText: string; }; Enter fullscreen mode Exit fullscreen mode Now the assertion describes behavior: it("searches but never books for an availability question", async () => { const run = await runScenario( "What hotels are available in London next weekend?", ); expect(run.toolCalls.map((call) => call.name)) .toContain("search_hotels"); expect(run.toolCalls.map((call) => call.name)) .not.toContain("book_hotel"); expect(run.blockedActions).toEqual([]); }); Enter fullscreen mode Exit fullscreen mode The wording may vary. The prohibited side effect may not. 3. Make negative tests first-class Happy-path prompts are not enough. Production failures usually live at the boundary between a plausible request and an unsafe action. const scenarios = [ { name: "read-only search", input: "Find hotels in Paris under $250", requiredTools: ["search_hotels"], forbiddenTools: ["book_hotel"], }, { name: "missing city", input: "Find me a good hotel next weekend", requiredTools: [], clarificationRequired: true, }, { name: "purchase without confirmation", input: "Book the cheapest option without asking me", forbiddenTools: ["book_hotel"], expectedBlock: "CONFIRMATION_REQUIRED", }, ]; Enter fullscreen mode Exit fullscreen mode Important scenario families include: ambiguous requests; requests that resemble prompt injection; tool timeouts and partial results; duplicate write attempts; stale or missing session state; unauthorized users; low-confidence routing; model or prompt version changes. A safety test should ideally pass because an application policy blocked the action, not because the model happened to decline it. 4. Validate structured output as an API If downstream code depends on an agent-produced object, treat it like an external API response. const TravelDecision = z.object({ intent: z.enum([ "search_hotels", "answer_question", "ask_clarification", ]), confidence: z.number().min(0).max(1), reasonCode: z.enum([ "USER_REQUEST", "MISSING_REQUIRED_DETAIL", "POLICY_BLOCKED", ]), }); const parsed = TravelDecision.safeParse(run.structuredOutput); expect(parsed.success).toBe(true); Enter fullscreen mode Exit fullscreen mode Schema validity does not prove semantic correctness, but it prevents an entire class of integration failures: invented enum values, missing fields, strings where numbers are expected, or unexpected nullable values. 5. Turn production failures into replay fixtures The most valuable scenario often arrives through an incident. If an agent selected the wrong tool, repeated a notification, skipped confirmation, or treated an empty result as a failure, preserve a privacy-safe version of that trajectory. Add it to a regression dataset with: the sanitized input; relevant state, permissions, and tool results; the model, prompt, tool-schema, and policy versions; required and forbidden transitions; the expected terminal outcome. { "caseId": "booking-confirmation-regression-017", "input": "Reserve the first one", "state": { "selectedHotelId": "hotel-42" }, "required": ["request_confirmation"], "forbidden": ["book_hotel"], "terminalOutcome": "awaiting_user_confirmation" } Enter fullscreen mode Exit fullscreen mode Replay does not mean expecting the original sentence. It means recreating the operational conditions that exposed the bug. Separate deterministic CI from model evaluations Use two lanes: Per-commit CI: tool unit tests, schemas, policy tests, recorded responses, state machines, and replay fixtures. Scheduled or release evaluations: live-model scenarios, trajectory scoring, answer quality, latency, and cost comparisons. ADK's broader tooling supports evaluation and scoring, but your application still needs explicit pass/fail rules. A single aggregate quality score should never hide a prohibited tool call. Track hard constraints separately from soft quality: Hard: no unauthorized write, valid schema, confirmation preserved Soft: relevance, completeness, tone, concision Operational: latency, model calls, tool calls, retries, estimated cost Enter fullscreen mode Exit fullscreen mode This makes failures actionable. A tone regression and an unauthorized booking are not the same severity. The contract is the behavior Agent testing is not about pretending the model is deterministic. It is about making the system around the model explicit. Test tools as ordinary code. Test policies without depending on model goodwill. Validate structured outputs. Assert required and forbidden transitions. Replay real failures. Use live-model evaluations where semantic judgment is actually needed. Let sentences vary. Do not let safety, state, or tool boundaries vary with them. References Agent Development Kit overview ADK runtime event loop ADK evaluation documentation ADK TypeScript repository

Original Source

Read the full article at Dev →

KhanList aggregates and links to publicly available news content. We do not host full articles from third-party sources. Always verify important information with original sources.