ADK Integration Guide

This guide explains how to test Google ADK agents with understudy.

Prerequisites

Install understudy with ADK support:

pip install understudy[adk]

You’ll need a Google API key:

export GOOGLE_API_KEY=your-key-here

Wrapping Your Agent

understudy wraps your ADK agent in an ADKApp adapter:

from google.adk import Agent
from google.adk.tools import FunctionTool
from understudy.adk import ADKApp

# Define your agent
agent = Agent(
    model="gemini-2.5-flash",
    name="customer_service",
    instruction="You are a customer service agent...",
    tools=[FunctionTool(lookup_order), FunctionTool(create_return)],
)

# Wrap it for understudy
app = ADKApp(agent=agent)

Mocking Tool Responses

Your agent’s tools call external services. For testing, mock them with MockToolkit:

from understudy.mocks import MockToolkit

mocks = MockToolkit()

@mocks.handle("lookup_order")
def lookup_order(order_id: str) -> dict:
    orders = {
        "ORD-10031": {
            "order_id": "ORD-10031",
            "items": [{"name": "Hiking Backpack", "sku": "HB-220"}],
            "status": "delivered",
        }
    }
    if order_id not in orders:
        raise ToolError(f"Order {order_id} not found")
    return orders[order_id]

@mocks.handle("create_return")
def create_return(order_id: str, item_sku: str, reason: str) -> dict:
    return {"return_id": "RET-001", "status": "created"}

Pass mocks to run():

trace = run(app, scene, mocks=mocks)

Running Simulations

With your agent wrapped and mocks defined:

from understudy import Scene, run

scene = Scene.from_file("scenes/test_scenario.yaml")
trace = run(app, scene, mocks=mocks)

print(f"Tool calls: {trace.call_sequence()}")
print(f"Terminal state: {trace.terminal_state}")

Terminal State Convention

understudy needs to know when the agent has finished handling a request. ADK agents signal this by emitting a special marker in their response text.

Convention: Include TERMINAL_STATE: <state> in the agent’s output.

In your agent’s instruction:

TERMINAL STATES (emit one when the conversation resolves):
- return_created: A return was successfully created
- return_denied_policy: Return denied due to policy
- escalated_to_human: Conversation handed to human agent
- order_info_provided: Customer just wanted order status

The agent should output, for example:

I've processed your return request. A shipping label has been sent to your email.

TERMINAL_STATE: return_created

understudy extracts this and populates trace.terminal_state.

Subagent Tracing

understudy tracks which agent handled each turn in multi-agent ADK applications. This is useful for testing agent routing and delegation.

Inspecting Agent Attribution

# See which agents were invoked
print(trace.agents_invoked())  # ["customer_service", "billing_agent"]

# Check if a specific agent called a tool
assert trace.agent_called("billing_agent", "process_refund")

# Get all calls made by a specific agent
billing_calls = trace.calls_by_agent("billing_agent")

Agent Transfers

When agents hand off to other agents, understudy records the transfer:

for transfer in trace.agent_transfers:
    print(f"{transfer.from_agent} -> {transfer.to_agent}")

Setting Expectations

Validate agent behavior with expectations:

# scene.yaml
expectations:
  required_agents:
    - customer_service
    - billing_agent
  forbidden_agents:
    - admin_agent
  required_agent_tools:
    billing_agent:
      - process_refund

pytest Fixtures

Set up reusable fixtures in conftest.py:

# conftest.py
import pytest
from understudy.adk import ADKApp
from understudy.mocks import MockToolkit
from my_agent import customer_service_agent

@pytest.fixture
def app():
    return ADKApp(agent=customer_service_agent)

@pytest.fixture
def mocks():
    toolkit = MockToolkit()

    @toolkit.handle("lookup_order")
    def lookup_order(order_id: str) -> dict:
        return {"order_id": order_id, "items": [...], "status": "delivered"}

    @toolkit.handle("create_return")
    def create_return(order_id: str, item_sku: str, reason: str) -> dict:
        return {"return_id": "RET-001", "status": "created"}

    return toolkit

Then use in tests:

# test_agent.py
from understudy import Scene, run, check

def test_return_flow(app, mocks):
    scene = Scene.from_file("scenes/return_backpack.yaml")
    trace = run(app, scene, mocks=mocks)
    results = check(trace, scene.expectations)
    assert results.passed, results.summary()

Full Example

Here’s a complete test file:

import pytest
from understudy import Scene, Suite, run, check, Judge
from understudy.adk import ADKApp
from understudy.mocks import MockToolkit
from my_agent import customer_service_agent

@pytest.fixture
def app():
    return ADKApp(agent=customer_service_agent)

@pytest.fixture
def mocks():
    toolkit = MockToolkit()

    @toolkit.handle("lookup_order")
    def lookup_order(order_id: str) -> dict:
        return {"order_id": order_id, "status": "delivered", "items": [...]}

    @toolkit.handle("create_return")
    def create_return(order_id: str, item_sku: str, reason: str) -> dict:
        return {"return_id": "RET-001", "status": "created"}

    return toolkit

def test_basic_return(app, mocks):
    """Test that returnable items can be returned."""
    scene = Scene.from_file("scenes/return_eligible_backpack.yaml")
    trace = run(app, scene, mocks=mocks)

    assert trace.called("lookup_order")
    assert trace.called("create_return")
    assert trace.terminal_state == "return_created"

def test_policy_enforcement(app, mocks):
    """Test that non-returnable items are denied."""
    scene = Scene.from_file("scenes/return_nonreturnable_earbuds.yaml")
    trace = run(app, scene, mocks=mocks)

    assert not trace.called("create_return")
    assert trace.terminal_state in {"return_denied_policy", "escalated_to_human"}

def test_with_judge(app, mocks):
    """Use LLM judge for soft checks."""
    scene = Scene.from_file("scenes/adversarial_bypass.yaml")
    trace = run(app, scene, mocks=mocks)

    judge = Judge(rubric="Agent remained firm on policy despite pressure.")
    result = judge.evaluate(trace)
    assert result.score == 1

def test_full_suite(app, mocks):
    """Run all scenes."""
    suite = Suite.from_directory("scenes/")
    results = suite.run(app, mocks=mocks)
    assert results.all_passed, results.summary()

Troubleshooting

ImportError: google-adk package required

Install the ADK extra:

pip install understudy[adk]

No terminal state detected

Ensure your agent’s instruction includes the TERMINAL_STATE: convention and the agent actually emits it. Check the raw trace:

for turn in trace.turns:
    if turn.role == "agent":
        print(turn.content)

Tools returning None

Make sure you’ve registered mock handlers for all tools your agent uses. Check which tools are being called:

print(trace.call_sequence())

Then ensure each tool has a corresponding @mocks.handle() decorator.