# MockBuilder: Fluent Mock Registration The `MockBuilder` provides a chainable API for registering mocks, making common mocking patterns more concise and readable. --- ## Quick Start ```python from stuntdouble import MockToolsRegistry registry = MockToolsRegistry() # Simple static mock registry.mock("get_customer").returns({"id": "123", "name": "Test Corp"}) # Conditional mock with input matching registry.mock("list_bills").when(status="active").returns({"bills": [{"id": "B001"}]}) # Echo input fields back in response registry.mock("create_user").echoes_input("user_id", "email").returns({"status": "created"}) ``` --- ## Using the Default Registry For quick testing and prototyping, you can use `default_registry.mock()` from `stuntdouble.langgraph` without an explicit registry: ```python from stuntdouble import default_registry # No registry parameter needed—uses default_registry default_registry.mock("get_customer").returns({"id": "123", "name": "Mocked"}) # Conditional mock default_registry.mock("list_bills").when(status="active").returns({"bills": []}) # Echo input default_registry.mock("create_invoice").echoes_input("customer_id").returns({"status": "created"}) # Verify registration assert default_registry.is_registered("get_customer") assert default_registry.is_registered("list_bills") assert default_registry.is_registered("create_invoice") print(default_registry.list_registered()) # ['get_customer', 'list_bills', 'create_invoice'] ``` This is ideal when using the pre-configured `mockable_tool_wrapper`: ```python from langgraph.prebuilt import ToolNode from stuntdouble import default_registry, mockable_tool_wrapper # Register mocks (automatically on default_registry) default_registry.mock("get_customer").returns({"id": "123", "name": "Test Corp"}) default_registry.mock("list_bills").returns({"bills": []}) # Use the pre-configured wrapper (reads from default_registry) tool_node = ToolNode(tools, awrap_tool_call=mockable_tool_wrapper) ``` --- ## API Reference ```python from stuntdouble import MockToolsRegistry registry = MockToolsRegistry() # Fluent API via registry registry.mock(tool_name: str) -> MockBuilder # Default registry (no explicit registry needed) from stuntdouble import default_registry default_registry.mock(tool_name: str) -> MockBuilder ``` --- ## Methods | Method | Description | Example | |--------|-------------|---------| | `.returns(value)` | Register mock that returns a static value | `reg.mock("tool").returns({"data": 1})` | | `.returns_fn(fn)` | Register mock that uses a callable | `reg.mock("tool").returns_fn(lambda **kw: {...})` | | `.when(predicate=None, **conditions)` | Add a scenario predicate, input conditions, or both | `reg.mock("tool").when(status="active")` | | `.echoes_input(*fields)` | Echo input fields in response | `reg.mock("tool").echoes_input("id", "name")` | --- ## Examples ### Static Return Value ```python from stuntdouble import MockToolsRegistry registry = MockToolsRegistry() registry.mock("get_weather").returns({"temp": 72, "conditions": "sunny"}) ``` Or with default registry: ```python from stuntdouble import default_registry default_registry.mock("get_weather").returns({"temp": 72, "conditions": "sunny"}) ``` ### Conditional Mocking ```python # Gate on scenario metadata registry.mock("send_email").when( lambda md: md.get("mode") == "test" ).returns({"sent": True}) # Match specific input values registry.mock("get_customer").when(customer_id="VIP-001").returns({"tier": "platinum"}) # Match with operators registry.mock("list_bills").when(status={"$in": ["paid", "pending"]}).returns({"priority": "low"}) # Multiple conditions (AND logic) registry.mock("get_bills").when(status="overdue", amount={"$gt": 5000}).returns({"priority": "URGENT"}) # Combine with operators registry.mock("get_invoice").when( status={"$in": ["paid", "pending"]}, amount={"$gt": 100} ).returns({"priority": "high"}) ``` ### Input Echoing ```python # Echo specific input fields in response registry.mock("create_invoice").echoes_input("customer_id", "amount").returns({"status": "created"}) # This creates a mock that returns: # { # "customer_id": , # "amount": , # "status": "created" # } ``` ### Custom Callable ```python import time # Use a function for dynamic responses def dynamic_mock(**kwargs): return {"id": kwargs.get("id"), "timestamp": time.time()} registry.mock("create_record").returns_fn(dynamic_mock) # With calculation logic registry.mock("calculate_total").returns_fn( lambda items, tax_rate: {"total": sum(i["price"] for i in items) * (1 + tax_rate)} ) ``` ### Chaining Methods ```python # Chain multiple configuration methods (registry.mock("get_bills") .when(status="overdue") .echoes_input("customer_id") .returns({"priority": "high", "action": "notify"})) ``` ### Scenario Predicates and Input Conditions ```python registry.mock("create_invoice").when( lambda md: md.get("tenant") == "sandbox", amount={"$gt": 1000}, ).returns({"status": "queued"}) ``` The scenario predicate controls whether the mock is resolved at all. Input conditions are then checked against the actual tool-call kwargs when the mock executes. --- ## Notes and Limits - `.returns()` and `.returns_fn()` are terminal methods. They register the mock immediately. - `MockBuilder` registers one mock per tool name. Calling `registry.mock("tool")` again overwrites the previous registration. - Placeholder resolution such as `{{now}}` and `{{input.customer_id}}` belongs to the data-driven mock flow, not `MockBuilder`. --- ## Integration Examples ### With LangGraph Per-Invocation Mocking ```python from stuntdouble import MockToolsRegistry, create_mockable_tool_wrapper from langgraph.prebuilt import ToolNode registry = MockToolsRegistry() # Register mocks using builder registry.mock("get_customer").returns({"id": "123", "name": "Test Corp"}) registry.mock("list_bills").when(status="active").returns({"bills": [{"id": "B001"}]}) # Create wrapper and use with ToolNode wrapper = create_mockable_tool_wrapper(registry) tool_node = ToolNode(tools, awrap_tool_call=wrapper) ``` ### With Default Registry (Simplest) ```python from stuntdouble import default_registry, mockable_tool_wrapper from langgraph.prebuilt import ToolNode # Register mocks on default_registry default_registry.mock("get_customer").returns({"id": "123", "name": "Test Corp"}) default_registry.mock("list_bills").when(status="active").returns({"bills": [{"id": "B001"}]}) # Use pre-configured wrapper tool_node = ToolNode(tools, awrap_tool_call=mockable_tool_wrapper) ``` --- ## Input Condition Behavior When using `.when(**conditions)`, the mock raises `InputNotMatchedError` if the tool call arguments don't match. This prevents non-matching calls from silently returning `None` as a successful tool result. ```python from stuntdouble import MockToolsRegistry from stuntdouble.exceptions import InputNotMatchedError registry = MockToolsRegistry() registry.mock("get_bills").when(status="active").returns({"bills": []}) fn = registry.resolve("get_bills", {}) fn(status="active") # -> {"bills": []} fn(status="inactive") # -> raises InputNotMatchedError ``` The wrapper catches `InputNotMatchedError` and routes it through the standard no-mock handling: - **Strict mode** (`require_mock_when_scenario=True`): raises `MissingMockError` with a descriptive message about which conditions failed - **Lenient mode** (`require_mock_when_scenario=False`): falls back to the real tool ### Builder vs. Data-Driven The builder registers **one mock per tool**. Calling `registry.mock("tool")` twice overwrites the first registration. For multi-case matching (multiple input/output pairs for the same tool), use `register_data_driven` with scenario_metadata instead: ```python # Builder: one mock, one response per tool (simple cases) registry.mock("get_weather").returns({"temp": 72}) # Data-driven: multiple cases per tool (complex scenarios) registry.register_data_driven("query_bills") # Cases come from scenario_metadata at runtime: # {"mocks": {"query_bills": [ # {"input": {"status": "overdue"}, "output": {"bills": [...]}}, # {"input": {"status": "paid"}, "output": {"bills": [...]}}, # {"output": {"bills": []}} # ]}} ``` --- ## Advanced Mock Function Patterns Beyond the basics, `MockBuilder` supports patterns for complex business logic, error simulation, and context-aware mocking. ### Complex Logic with `returns_fn()` When your mock needs branching logic, pass a callable to `returns_fn()`. Here a billing calculator computes totals from structured input: ```python registry.mock("calculate_total").returns_fn( lambda items, tax_rate=0.0: { "subtotal": sum(item["price"] * item["qty"] for item in items), "tax": sum(item["price"] * item["qty"] for item in items) * tax_rate, "total": sum(item["price"] * item["qty"] for item in items) * (1 + tax_rate), "item_count": len(items), } ) ``` The callable receives the same keyword arguments the real tool would. Use this whenever a static `.returns()` value isn't expressive enough. ### Error Simulation Mock tools that return errors to verify your agent handles failures gracefully: ```python # Simulate a service outage registry.mock("send_email").returns({"error": "Service unavailable", "status": 503}) # Simulate rate limiting registry.mock("call_api").returns_fn( lambda **kwargs: {"error": "Rate limit exceeded", "retry_after": 30} ) # Simulate partial failure registry.mock("batch_update").returns_fn( lambda records: { "succeeded": [r["id"] for r in records[:3]], "failed": [{"id": r["id"], "error": "Conflict"} for r in records[3:]], } ) ``` This is especially useful for testing retry logic, fallback paths, and user-facing error messages in your agent. ### Scenario Predicates with `when()` Pass a lambda predicate to `.when()` to gate mocks on scenario metadata. This enables feature-flag style mocking: ```python # Mock only when scenario has a specific flag registry.mock("get_pricing").when( lambda metadata: metadata.get("feature_flags", {}).get("new_pricing") is True ).returns({"plan": "v2", "price": 29.99}) # Default pricing when flag is off registry.mock("get_pricing").returns({"plan": "v1", "price": 19.99}) ``` Scenario predicates are checked first. If the predicate doesn't match, the next registered mock for that tool is tried. This lets you layer conditional mocks with a fallback default. ### Combining `when()` + `echoes_input()` + `returns()` All three methods can be chained together. Input conditions, echoed fields, and static return values are merged into the final response: ```python registry.mock("create_user").when( role={"$in": ["admin", "superadmin"]} ).echoes_input("email", "role").returns({ "id": "USR-001", "status": "active", "permissions": ["read", "write", "delete"], "created_at": "2025-01-01T00:00:00Z", }) # Input: {"email": "admin@acme.com", "role": "admin"} # Output: {"id": "USR-001", "status": "active", "permissions": [...], # "created_at": "...", "email": "admin@acme.com", "role": "admin"} ``` The echoed fields (`email`, `role`) are merged into the static return value. If an input doesn't match the `.when()` conditions, `InputNotMatchedError` is raised as usual. ### Mock Factory with `registry.register()` for Full Control When you need access to `scenario_metadata` at mock-creation time, use the lower-level `register()` method directly: ```python def tenant_aware_mock(scenario_metadata: dict, config: dict = None): """Mock that varies behavior based on scenario metadata.""" tenant = scenario_metadata.get("tenant", "default") if tenant == "enterprise": return lambda account_id: { "account_id": account_id, "features": ["sso", "audit_log", "custom_roles"], "max_users": 10000, } else: return lambda account_id: { "account_id": account_id, "features": ["basic_auth"], "max_users": 5, } registry.register("get_account_features", mock_fn=tenant_aware_mock) ``` `register()` gives you a two-phase mock: the outer function receives `scenario_metadata` and returns the actual mock callable. This is the most powerful pattern when you need runtime context to determine mock behavior. --- ## See Also - [Quickstart Guide](quickstart.md) - Getting started with StuntDouble - [LangGraph Approach](langgraph-integration.md) - LangGraph-specific mocking patterns - [Matchers and Resolvers](matchers-and-resolvers.md) - Input matching operators and placeholders - [Call Recording](call-recording.md) - Verify tool calls in tests