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 tool=

Set validate_signatures=False or omit tools=

Registration only (fail fast)

Pass tool=real_tool

Set validate_signatures=False or omit tools=

Runtime only (lazy validation)

Omit tool=

Pass tools=all_tools + validate_signatures=True

Both (belt and suspenders)

Pass tool=real_tool

Pass tools=all_tools + validate_signatures=True


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

strict_mock_errors

Behavior on mock exception

Lenient (default)

False

Returns ToolMessage(status="error"), agent continues

Strict

True

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:

  1. Missing parameters: Mock is missing parameters the tool expects

  2. Extra required parameters: Mock requires parameters the tool doesn’t have

  3. 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 model_fields inspection

JSON Schema dict

MCP tools via langchain-mcp-adapters

Parses properties and required fields

# 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_mock_signature(tool, mock_fn, scenario_metadata, config)

Validate that a mock function’s parameters match the real tool’s signature

validate_mock_parameters(tool, cases)

Validate that mock input patterns match the tool’s input schema

validate_registry_mocks(tools, scenario_metadata)

Validate all runtime mock cases against the provided tools

SignatureMismatchError

Exception raised when validation fails


See Also