Mock Signature Validation Guide
StuntDouble can validate that your mock functions have the same parameter signature as the real tools they’re mocking. This catches configuration errors early—either at registration time (fail-fast) or at runtime (safety net).
Overview
┌─────────────────────────────────────────────────────────────────────────────┐
│ REGISTRATION TIME │
│ registry.register("tool_name", mock_fn=..., tool=real_tool) │
│ │ │
│ tool provided? │
│ / \ │
│ Yes No │
│ │ │ │
│ validate_mock_signature() └──► Register without validation │
│ │ │
│ Valid? ──No──► SignatureMismatchError (fail fast) │
│ │ │
│ Yes │
│ ▼ │
│ Registered ✓ │
└─────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────┐
│ RUNTIME │
│ Tool call intercepted by mockable_tool_wrapper │
│ │ │
│ validate_signatures=True AND tool in tools? │
│ / \ │
│ Yes No │
│ │ │ │
│ validate_mock_signature() Execute mock │
│ │ │
│ Valid? ──No──► Raise SignatureMismatchError │
│ │ │
│ Yes │
│ │ │
│ ▼ │
│ Execute mock → ToolMessage │
└─────────────────────────────────────────────────────────────────────────────┘
Validation Options
There are two independent validation points with separate controls:
What you want |
Registration |
Wrapper |
|---|---|---|
No validation at all |
Omit |
Set |
Registration only (fail fast) |
Pass |
Set |
Runtime only (lazy validation) |
Omit |
Pass |
Both (belt and suspenders) |
Pass |
Pass |
Registration-Time Validation
Validates immediately when you register the mock. Throws SignatureMismatchError if signatures don’t match:
from stuntdouble import MockToolsRegistry, SignatureMismatchError
registry = MockToolsRegistry()
# This mock is missing the 'units' parameter
def bad_weather_mock(scenario_metadata):
def mock_fn(city: str): # Missing 'units' parameter!
return {"temp": 72}
return mock_fn
try:
registry.register(
"get_weather",
mock_fn=bad_weather_mock,
tool=get_weather_tool, # ← Passing tool enables validation
)
except SignatureMismatchError as e:
print(f"Registration failed: {e}")
# "Mock for 'get_weather' has mismatched signature.
# Missing parameters in mock: units"
How to Enable
Pass the tool= parameter to registry.register():
# ✅ Validation enabled — catches errors at registration
registry.register("get_weather", mock_fn=my_mock, tool=get_weather_tool)
# ❌ No validation — errors found later at runtime (or never)
registry.register("get_weather", mock_fn=my_mock)
Runtime Validation
Validates each time a mock is about to be executed. On failure, the wrapper raises SignatureMismatchError:
from langgraph.prebuilt import ToolNode
from stuntdouble import MockToolsRegistry, create_mockable_tool_wrapper
registry = MockToolsRegistry()
registry.register("get_weather", mock_fn=weather_mock) # No tool= means no registration validation
# Create wrapper with runtime validation enabled
wrapper = create_mockable_tool_wrapper(
registry,
tools=all_tools, # ← List of tools for validation lookup
validate_signatures=True, # ← Enable runtime validation (default)
)
# If mock signature doesn't match at runtime:
# → Raises SignatureMismatchError before the mock executes
node = ToolNode(all_tools, awrap_tool_call=wrapper)
How to Enable
Pass tools= and validate_signatures=True to create_mockable_tool_wrapper():
# ✅ Runtime validation enabled
wrapper = create_mockable_tool_wrapper(
registry,
tools=all_tools,
validate_signatures=True, # Default is True when tools= is provided
)
# ❌ Runtime validation disabled
wrapper = create_mockable_tool_wrapper(
registry,
validate_signatures=False,
)
Validating Scenario Metadata Cases
Use validate_registry_mocks() to validate scenario_metadata["mocks"] cases against the real tool parameter names before invoking the graph:
from stuntdouble import validate_registry_mocks
errors = validate_registry_mocks(all_tools, scenario_metadata)
if errors:
raise ValueError(errors)
This is especially useful for data-driven mocks where cases are supplied at runtime.
Strict Mock Errors
By default, exceptions raised during mock execution are caught and returned as ToolMessage(status="error"), allowing evaluation batch runs to continue collecting results. For unit tests where broken mocks should fail fast, enable strict_mock_errors:
wrapper = create_mockable_tool_wrapper(
registry,
strict_mock_errors=True, # Re-raise mock execution errors
)
Mode |
|
Behavior on mock exception |
|---|---|---|
Lenient (default) |
|
Returns |
Strict |
|
Re-raises the exception, test fails immediately |
Opting Out
Skip all validation when you trust your mocks or during development:
# Skip registration validation
registry.register("get_weather", mock_fn=weather_mock) # No tool=
# Skip runtime validation
wrapper = create_mockable_tool_wrapper(
registry,
validate_signatures=False, # ← Disable runtime validation
)
What Gets Validated
The validate_mock_signature function checks:
Missing parameters: Mock is missing parameters the tool expects
Extra required parameters: Mock requires parameters the tool doesn’t have
Required/optional mismatch: Tool has optional param but mock requires it
# Tool signature: get_weather(city: str, units: str = "celsius")
# ✅ Valid - exact match
def mock_fn(city: str, units: str = "celsius"): ...
# ✅ Valid - optional params can have different defaults
def mock_fn(city: str, units: str = "fahrenheit"): ...
# ❌ Invalid - missing 'units' parameter
def mock_fn(city: str): ...
# ❌ Invalid - extra required parameter
def mock_fn(city: str, units: str = "celsius", extra: str): ...
# ❌ Invalid - 'units' should be optional but mock requires it
def mock_fn(city: str, units: str): ...
MCP Tool Support
Signature validation works with MCP tools loaded via langchain-mcp-adapters. These tools provide JSON Schema dicts instead of Pydantic models, and StuntDouble handles both formats:
Schema Format |
Source |
Handling |
|---|---|---|
Pydantic model |
LangChain native tools |
Standard |
JSON Schema dict |
MCP tools via langchain-mcp-adapters |
Parses |
# MCP tools work seamlessly with validation
from langchain_mcp_adapters.client import MultiServerMCPClient
client = MultiServerMCPClient({"mcp-server": {"url": "http://localhost:8080/mcp"}})
mcp_tools = await client.get_tools() # Tools have JSON Schema args_schema
# StuntDouble validates against JSON Schema automatically
wrapper = create_mockable_tool_wrapper(
registry,
tools=mcp_tools, # ← MCP tools with JSON Schema
validate_signatures=True, # ← Works with JSON Schema dicts
)
Complete Example
Belt-and-Suspenders Validation
from stuntdouble import (
MockToolsRegistry,
create_mockable_tool_wrapper,
SignatureMismatchError,
)
# Your real tools
all_tools = [get_weather_tool, get_customer_tool, list_bills_tool]
registry = MockToolsRegistry()
# Registration-time validation (fail fast)
try:
registry.mock("get_weather").returns({"temp": 72, "conditions": "sunny"})
# Note: .returns() creates a lambda that accepts **kwargs, so signature
# validation needs the tool= parameter for manual factories
# Manual factory with validation
def weather_mock(md):
def fn(city: str, units: str = "celsius"):
return {"temp": 72, "conditions": "sunny", "city": city, "units": units}
return fn
registry.register("get_weather", mock_fn=weather_mock, tool=get_weather_tool)
except SignatureMismatchError as e:
print(f"Fix your mock: {e}")
# Runtime validation (safety net)
wrapper = create_mockable_tool_wrapper(
registry,
tools=all_tools,
validate_signatures=True,
)
tool_node = ToolNode(all_tools, awrap_tool_call=wrapper)
pytest Integration
import pytest
from stuntdouble import MockToolsRegistry, SignatureMismatchError
def test_mock_signature_validation():
"""Test that invalid mock signatures are caught."""
registry = MockToolsRegistry()
def bad_mock(md):
def fn(city: str): # Missing 'units' parameter
return {"temp": 72}
return fn
with pytest.raises(SignatureMismatchError, match="Missing parameters"):
registry.register("get_weather", mock_fn=bad_mock, tool=get_weather_tool)
def test_valid_mock_signature():
"""Test that valid mock signatures pass validation."""
registry = MockToolsRegistry()
def good_mock(md):
def fn(city: str, units: str = "celsius"):
return {"temp": 72, "city": city, "units": units}
return fn
# Should not raise
registry.register("get_weather", mock_fn=good_mock, tool=get_weather_tool)
assert registry.is_registered("get_weather")
API Reference
from stuntdouble import (
validate_mock_signature, # Validate mock function signature matches tool
validate_mock_parameters, # Validate mock inputs match tool schema
validate_registry_mocks, # Validate scenario_metadata mocks against tools
SignatureMismatchError, # Raised when mock signature doesn't match tool
)
Function |
Description |
|---|---|
|
Validate that a mock function’s parameters match the real tool’s signature |
|
Validate that mock input patterns match the tool’s input schema |
|
Validate all runtime mock cases against the provided tools |
|
Exception raised when validation fails |
See Also
Quickstart Guide — Getting started with StuntDouble
LangGraph Approach — Per-invocation mocking
Call Recording — Verify tool calls in tests
Mock Input Validation Reference — Validate scenario metadata and tool parameters