"""
Pattern 267: Test Double Pattern with LangChain/LangGraph
Purpose: Comprehensive test double implementations (dummy, fake, spy, mock, stub).
Provides intelligent test double orchestration with LLM-powered behavior analysis.
"""
from typing import Dict, Any, List, Optional, Callable, TypedDict
from dataclasses import dataclass, field
from datetime import datetime
from enum import Enum
from collections import defaultdict
from langchain_core.prompts import ChatPromptTemplate
from langchain_openai import ChatOpenAI
from langgraph.graph import StateGraph, END
import json
class DoubleType(Enum):
"""Types of test doubles."""
DUMMY = "dummy" # No logic, fills parameter
FAKE = "fake" # Working implementation, simplified
SPY = "spy" # Records calls to real object
MOCK = "mock" # Pre-programmed expectations
STUB = "stub" # Pre-programmed responses
class VerificationStatus(Enum):
"""Status of expectation verification."""
NOT_VERIFIED = "not_verified"
PASSED = "passed"
FAILED = "failed"
@dataclass
class Expectation:
"""Mock expectation."""
method: str
expected_calls: int
expected_args: Optional[tuple] = None
expected_kwargs: Optional[Dict[str, Any]] = None
return_value: Any = None
@dataclass
class CallRecord:
"""Record of method call."""
method: str
args: tuple
kwargs: Dict[str, Any]
result: Any
double_type: DoubleType
timestamp: datetime = field(default_factory=datetime.now)
@dataclass
class DoubleAnalysis:
"""Analysis of test double usage."""
recommended_type: DoubleType
reasoning: str
complexity_score: float
usage_patterns: List[str]
class TestDoubleState(TypedDict):
"""State for test double workflow."""
method_name: str
args: tuple
kwargs: Dict[str, Any]
double_type: DoubleType
analysis: Optional[DoubleAnalysis]
result: Any
verification_status: VerificationStatus
expectations_met: bool
insights: List[str]
call_count: int
class IntelligentTestDouble:
"""
Intelligent Test Double with LangGraph workflow integration.
Features:
- All test double types (dummy, fake, spy, mock, stub)
- LLM-powered behavior analysis
- Expectation verification
- Call recording and replay
- Pattern detection
- Intelligent recommendations
"""
def __init__(self, name: str, double_type: DoubleType, model_name: str = "gpt-4", temperature: float = 0.5):
self.name = name
self.double_type = double_type
self.llm = ChatOpenAI(model=model_name, temperature=temperature)
# Configuration
self.methods: Dict[str, Any] = {}
self.expectations: Dict[str, List[Expectation]] = defaultdict(list)
self.fake_implementations: Dict[str, Callable] = {}
# Tracking
self.call_records: List[CallRecord] = []
self.call_counts: Dict[str, int] = defaultdict(int)
# Build workflow
self.workflow = self._build_workflow()
# Statistics
self.stats = {
"total_calls": 0,
"verifications_passed": 0,
"verifications_failed": 0,
"double_type": double_type.value
}
def _build_workflow(self) -> StateGraph:
"""Build LangGraph workflow."""
workflow = StateGraph(TestDoubleState)
workflow.add_node("analyze_double_type", self._analyze_double_type_node)
workflow.add_node("configure_behavior", self._configure_behavior_node)
workflow.add_node("execute_call", self._execute_call_node)
workflow.add_node("verify_expectations", self._verify_expectations_node)
workflow.add_node("generate_report", self._generate_report_node)
workflow.set_entry_point("analyze_double_type")
workflow.add_edge("analyze_double_type", "configure_behavior")
workflow.add_edge("configure_behavior", "execute_call")
workflow.add_edge("execute_call", "verify_expectations")
workflow.add_edge("verify_expectations", "generate_report")
workflow.add_edge("generate_report", END)
return workflow.compile()
def _analyze_double_type_node(self, state: TestDoubleState) -> TestDoubleState:
"""Analyze test double type and usage."""
method_name = state["method_name"]
args = state["args"]
kwargs = state["kwargs"]
# Increment call count
self.call_counts[method_name] += 1
state["call_count"] = self.call_counts[method_name]
# Use LLM to analyze if this is first call to method
if self.call_counts[method_name] == 1 and not self.methods.get(method_name):
prompt = ChatPromptTemplate.from_messages([
("system", "You are an expert in test double patterns and testing strategies."),
("user", """Analyze this method call for test double usage:
Method: {method_name}
Arguments: {args}
Keyword Arguments: {kwargs}
Current Double Type: {double_type}
Provide analysis as JSON with:
- recommended_type: Best double type for this method
- reasoning: Why this type is recommended
- complexity_score: 0.0-1.0 score for implementation complexity
- usage_patterns: List of observed usage patterns
Double types: dummy (parameter filler), fake (simplified working), spy (record calls), mock (verify expectations), stub (fixed responses)""")
])
chain = prompt | self.llm
response = chain.invoke({
"method_name": method_name,
"args": str(args),
"kwargs": str(kwargs),
"double_type": self.double_type.value
})
try:
analysis_data = json.loads(response.content)
state["analysis"] = DoubleAnalysis(
recommended_type=DoubleType(analysis_data.get("recommended_type", self.double_type.value)),
reasoning=analysis_data.get("reasoning", "Default reasoning"),
complexity_score=analysis_data.get("complexity_score", 0.5),
usage_patterns=analysis_data.get("usage_patterns", [])
)
except:
state["analysis"] = DoubleAnalysis(
recommended_type=self.double_type,
reasoning="Using configured double type",
complexity_score=0.5,
usage_patterns=[]
)
return state
def _configure_behavior_node(self, state: TestDoubleState) -> TestDoubleState:
"""Configure double behavior."""
method_name = state["method_name"]
# Store method configuration if not exists
if method_name not in self.methods:
self.methods[method_name] = {
"return_value": None,
"configured": False
}
return state
def _execute_call_node(self, state: TestDoubleState) -> TestDoubleState:
"""Execute the test double call."""
method_name = state["method_name"]
args = state["args"]
kwargs = state["kwargs"]
double_type = state["double_type"]
result = None
if double_type == DoubleType.DUMMY:
# Dummy: Do nothing, return None
result = None
elif double_type == DoubleType.STUB:
# Stub: Return configured value
method_config = self.methods.get(method_name, {})
result = method_config.get("return_value")
elif double_type == DoubleType.FAKE:
# Fake: Use simplified implementation
if method_name in self.fake_implementations:
try:
result = self.fake_implementations[method_name](*args, **kwargs)
except Exception as e:
result = {"error": str(e)}
else:
# Default fake behavior
result = {"status": "fake_success", "method": method_name, "data": "fake_data"}
elif double_type == DoubleType.SPY:
# Spy: Record call and execute real implementation if available
if method_name in self.fake_implementations:
try:
result = self.fake_implementations[method_name](*args, **kwargs)
except Exception as e:
result = {"error": str(e)}
else:
result = {"status": "spy_recorded", "method": method_name}
elif double_type == DoubleType.MOCK:
# Mock: Return expected value if configured
expectations = self.expectations.get(method_name, [])
if expectations:
# Use first expectation's return value
result = expectations[0].return_value
else:
result = {"status": "mock_success", "method": method_name}
state["result"] = result
# Record call
call_record = CallRecord(
method=method_name,
args=args,
kwargs=kwargs,
result=result,
double_type=double_type
)
self.call_records.append(call_record)
self.stats["total_calls"] += 1
return state
def _verify_expectations_node(self, state: TestDoubleState) -> TestDoubleState:
"""Verify expectations for mocks."""
method_name = state["method_name"]
args = state["args"]
kwargs = state["kwargs"]
expectations_met = True
if state["double_type"] == DoubleType.MOCK:
expectations = self.expectations.get(method_name, [])
for expectation in expectations:
# Check call count
actual_calls = self.call_counts[method_name]
if actual_calls != expectation.expected_calls:
expectations_met = False
state["insights"] = state.get("insights", [])
state["insights"].append(
f"Expected {expectation.expected_calls} calls to {method_name}, got {actual_calls}"
)
# Check arguments if specified
if expectation.expected_args is not None and args != expectation.expected_args:
expectations_met = False
state["insights"] = state.get("insights", [])
state["insights"].append(
f"Expected args {expectation.expected_args}, got {args}"
)
if expectation.expected_kwargs is not None and kwargs != expectation.expected_kwargs:
expectations_met = False
state["insights"] = state.get("insights", [])
state["insights"].append(
f"Expected kwargs {expectation.expected_kwargs}, got {kwargs}"
)
state["expectations_met"] = expectations_met
state["verification_status"] = VerificationStatus.PASSED if expectations_met else VerificationStatus.FAILED
if expectations_met:
self.stats["verifications_passed"] += 1
else:
self.stats["verifications_failed"] += 1
return state
def _generate_report_node(self, state: TestDoubleState) -> TestDoubleState:
"""Generate insights report."""
if not state.get("insights"):
state["insights"] = []
# Add summary insight
state["insights"].append(
f"Test double '{self.name}' of type {state['double_type'].value} executed {state['method_name']}"
)
return state
def configure_method(
self,
method_name: str,
return_value: Any = None,
implementation: Optional[Callable] = None
):
"""Configure method behavior."""
self.methods[method_name] = {
"return_value": return_value,
"configured": True
}
if implementation:
self.fake_implementations[method_name] = implementation
def expect_call(
self,
method_name: str,
times: int = 1,
with_args: Optional[tuple] = None,
with_kwargs: Optional[Dict[str, Any]] = None,
returns: Any = None
):
"""Set expectation for mock."""
expectation = Expectation(
method=method_name,
expected_calls=times,
expected_args=with_args,
expected_kwargs=with_kwargs,
return_value=returns
)
self.expectations[method_name].append(expectation)
def call_method(self, method_name: str, *args, **kwargs) -> Any:
"""Execute method call through workflow."""
initial_state: TestDoubleState = {
"method_name": method_name,
"args": args,
"kwargs": kwargs,
"double_type": self.double_type,
"analysis": None,
"result": None,
"verification_status": VerificationStatus.NOT_VERIFIED,
"expectations_met": True,
"insights": [],
"call_count": 0
}
final_state = self.workflow.invoke(initial_state)
return final_state["result"]
def verify(self, method_name: str, times: Optional[int] = None) -> bool:
"""Verify method was called expected times."""
actual_calls = self.call_counts[method_name]
if times is None:
return actual_calls > 0
return actual_calls == times
def get_calls(self, method_name: Optional[str] = None) -> List[CallRecord]:
"""Get call records."""
if method_name:
return [call for call in self.call_records if call.method == method_name]
return self.call_records
def reset(self):
"""Reset double state."""
self.call_records.clear()
self.call_counts.clear()
self.stats["total_calls"] = 0
self.stats["verifications_passed"] = 0
self.stats["verifications_failed"] = 0
def get_statistics(self) -> Dict[str, Any]:
"""Get test double statistics."""
return {
"name": self.name,
**self.stats,
"methods_configured": len(self.methods),
"expectations_set": sum(len(exps) for exps in self.expectations.values())
}
class TestDoublePattern:
"""
Comprehensive Test Double Pattern coordinator with LangChain/LangGraph.
"""
def __init__(self, model_name: str = "gpt-4"):
self.doubles: Dict[str, IntelligentTestDouble] = {}
self.model_name = model_name
def create_dummy(self, name: str) -> IntelligentTestDouble:
"""Create a dummy double."""
double = IntelligentTestDouble(name, DoubleType.DUMMY, model_name=self.model_name)
self.doubles[name] = double
return double
def create_stub(self, name: str) -> IntelligentTestDouble:
"""Create a stub double."""
double = IntelligentTestDouble(name, DoubleType.STUB, model_name=self.model_name)
self.doubles[name] = double
return double
def create_fake(self, name: str) -> IntelligentTestDouble:
"""Create a fake double."""
double = IntelligentTestDouble(name, DoubleType.FAKE, model_name=self.model_name)
self.doubles[name] = double
return double
def create_spy(self, name: str) -> IntelligentTestDouble:
"""Create a spy double."""
double = IntelligentTestDouble(name, DoubleType.SPY, model_name=self.model_name)
self.doubles[name] = double
return double
def create_mock(self, name: str) -> IntelligentTestDouble:
"""Create a mock double."""
double = IntelligentTestDouble(name, DoubleType.MOCK, model_name=self.model_name)
self.doubles[name] = double
return double
def call(self, double_name: str, method_name: str, *args, **kwargs) -> Dict[str, Any]:
"""Execute method on double."""
if double_name not in self.doubles:
return {"error": f"Double {double_name} not found"}
double = self.doubles[double_name]
result = double.call_method(method_name, *args, **kwargs)
return {
"double": double_name,
"type": double.double_type.value,
"method": method_name,
"result": result,
"call_count": double.call_counts[method_name]
}
def get_statistics(self) -> Dict[str, Any]:
"""Get overall statistics."""
total_calls = sum(d.stats["total_calls"] for d in self.doubles.values())
total_verifications_passed = sum(d.stats["verifications_passed"] for d in self.doubles.values())
total_verifications_failed = sum(d.stats["verifications_failed"] for d in self.doubles.values())
return {
"total_doubles": len(self.doubles),
"total_calls": total_calls,
"verifications_passed": total_verifications_passed,
"verifications_failed": total_verifications_failed
}
if __name__ == "__main__":
print("=== Test Double Pattern with LangChain/LangGraph Demo ===\n")
print("Scenario 1: Dummy Double (Parameter Filler)")
print("-" * 50)
pattern = TestDoublePattern(model_name="gpt-4")
dummy = pattern.create_dummy("DummyLogger")
print("\nCalling dummy methods (no functionality)...")
result1 = pattern.call("DummyLogger", "log", message="Test message")
print(f"Result: {result1['result']}")
result2 = pattern.call("DummyLogger", "debug", message="Debug info")
print(f"Result: {result2['result']}")
print("\n" + "=" * 50)
print("\nScenario 2: Stub Double (Fixed Responses)")
print("-" * 50)
stub = pattern.create_stub("DatabaseStub")
# Configure stub responses
stub.configure_method("get_user", return_value={"id": 1, "name": "Test User", "email": "test@example.com"})
stub.configure_method("get_count", return_value=42)
print("\nCalling stub methods...")
result3 = pattern.call("DatabaseStub", "get_user", user_id=1)
print(f"get_user result: {result3['result']}")
result4 = pattern.call("DatabaseStub", "get_count")
print(f"get_count result: {result4['result']}")
print("\n" + "=" * 50)
print("\nScenario 3: Fake Double (Simplified Implementation)")
print("-" * 50)
fake = pattern.create_fake("FakeEmailService")
# Configure fake implementation
def fake_send_email(to: str, subject: str, body: str = "") -> Dict[str, Any]:
return {
"status": "sent",
"message_id": f"FAKE_{hash(to + subject)}",
"to": to,
"subject": subject
}
fake.configure_method("send_email", implementation=fake_send_email)
print("\nCalling fake email service...")
result5 = pattern.call("FakeEmailService", "send_email",
to="user@example.com",
subject="Test Email",
body="This is a test")
print(f"Result: {result5['result']}")
print("\n" + "=" * 50)
print("\nScenario 4: Spy Double (Record Calls)")
print("-" * 50)
spy = pattern.create_spy("MetricsSpy")
# Configure spy implementation
def track_metric(metric_name: str, value: float) -> Dict[str, Any]:
return {
"tracked": True,
"metric": metric_name,
"value": value,
"timestamp": datetime.now().isoformat()
}
spy.configure_method("track", implementation=track_metric)
print("\nCalling spy methods (recording calls)...")
result6 = pattern.call("MetricsSpy", "track", metric_name="page_views", value=100.0)
print(f"Result: {result6['result']}")
result7 = pattern.call("MetricsSpy", "track", metric_name="conversions", value=5.0)
print(f"Result: {result7['result']}")
# Show recorded calls
print(f"\nRecorded calls to 'track': {spy.call_counts['track']}")
calls = spy.get_calls("track")
for i, call in enumerate(calls, 1):
print(f" Call {i}: args={call.args}, kwargs={call.kwargs}")
print("\n" + "=" * 50)
print("\nScenario 5: Mock Double (Expectation Verification)")
print("-" * 50)
mock = pattern.create_mock("PaymentMock")
# Set expectations
mock.expect_call(
"process_payment",
times=2,
returns={"status": "success", "transaction_id": "TXN123"}
)
print("\nCalling mock methods (with expectations)...")
result8 = pattern.call("PaymentMock", "process_payment", amount=100.0)
print(f"Result: {result8['result']}")
result9 = pattern.call("PaymentMock", "process_payment", amount=50.0)
print(f"Result: {result9['result']}")
# Verify expectations
print(f"\nVerifying 'process_payment' called 2 times: {mock.verify('process_payment', times=2)}")
print("\n" + "=" * 50)
print("\nScenario 6: Multiple Calls and Pattern Detection")
print("-" * 50)
multi_stub = pattern.create_stub("ConfigStub")
multi_stub.configure_method("get_setting", return_value={"timeout": 30})
print("\nMaking multiple calls to detect patterns...")
for i in range(3):
result = pattern.call("ConfigStub", "get_setting", key="timeout")
print(f"Call {i+1}: {result['result']}")
print("\n" + "=" * 50)
print("\nStatistics")
print("-" * 50)
overall_stats = pattern.get_statistics()
print(f"\nOverall Statistics:")
print(f" Total Doubles: {overall_stats['total_doubles']}")
print(f" Total Calls: {overall_stats['total_calls']}")
print(f" Verifications Passed: {overall_stats['verifications_passed']}")
print(f" Verifications Failed: {overall_stats['verifications_failed']}")
print(f"\nIndividual Double Statistics:")
for name, double in pattern.doubles.items():
stats = double.get_statistics()
print(f"\n{name} ({stats['double_type']}):")
print(f" Total Calls: {stats['total_calls']}")
print(f" Methods Configured: {stats['methods_configured']}")
print(f" Expectations Set: {stats['expectations_set']}")
print("\n" + "=" * 50)
print("Test double pattern demonstration complete!")
print("=" * 50)
Agentic Architecture Pattern: Test Double
Pattern 267: Test Double Pattern – Line-by-Line Explanation
Overview
This pattern implements comprehensive test double implementations (dummy, fake, spy, mock, stub) with intelligent orchestration and LLM-powered behavior analysis. Test doubles are objects that stand in for real objects during testing.
Code Explanation
Lines 1-17: Imports
"""
Pattern 267: Test Double Pattern with LangChain/LangGraph
Purpose: Comprehensive test double implementations (dummy, fake, spy, mock, stub).
Provides intelligent test double orchestration with LLM-powered behavior analysis.
"""
Explanation: Module docstring – this pattern provides all 5 types of test doubles from Martin Fowler’s testing taxonomy.
from typing import Dict, Any, List, Optional, Callable, TypedDict
from dataclasses import dataclass, field
from datetime import datetime
from enum import Enum
from collections import defaultdict
from langchain_core.prompts import ChatPromptTemplate
from langchain_openai import ChatOpenAI
from langgraph.graph import StateGraph, END
import json
Explanation: Standard imports plus json for parsing LLM responses.
Lines 19-30: Enum Definitions
class DoubleType(Enum):
"""Types of test doubles."""
DUMMY = "dummy" # No logic, fills parameter
FAKE = "fake" # Working implementation, simplified
SPY = "spy" # Records calls to real object
MOCK = "mock" # Pre-programmed expectations
STUB = "stub" # Pre-programmed responses
Explanation: Five test double types: – DUMMY: Fills parameter slots, never actually used (e.g., null object) – FAKE: Simplified working implementation (e.g., in-memory database) – SPY: Wraps real object and records calls for verification – MOCK: Has expectations about how it should be called – STUB: Returns canned responses (like pattern 266)
class VerificationStatus(Enum):
"""Status of expectation verification."""
NOT_VERIFIED = "not_verified"
PASSED = "passed"
FAILED = "failed"
Explanation: Tracks whether mock expectations were verified.
Lines 32-71: Dataclasses
@dataclass
class Expectation:
"""Mock expectation."""
method: str
expected_calls: int
expected_args: Optional[tuple] = None
expected_kwargs: Optional[Dict[str, Any]] = None
return_value: Any = None
Explanation: Defines what a mock expects: – How many times method should be called – What arguments it should receive – What it should return
Example: Expect process_payment called exactly 2 times with amount > 0.
@dataclass
class CallRecord:
"""Record of method call."""
method: str
args: tuple
kwargs: Dict[str, Any]
result: Any
double_type: DoubleType
timestamp: datetime = field(default_factory=datetime.now)
Explanation: Records each method invocation for spies and verification.
@dataclass
class DoubleAnalysis:
"""Analysis of test double usage."""
recommended_type: DoubleType
reasoning: str
complexity_score: float
usage_patterns: List[str]
Explanation: LLM analysis recommending best double type based on usage patterns.
Lines 73-85: TypedDict State
class TestDoubleState(TypedDict):
"""State for test double workflow."""
method_name: str
args: tuple
kwargs: Dict[str, Any]
double_type: DoubleType
analysis: Optional[DoubleAnalysis]
result: Any
verification_status: VerificationStatus
expectations_met: bool
insights: List[str]
call_count: int
Explanation: Workflow state tracking method call, analysis, result, and verification status.
Lines 87-137: IntelligentTestDouble Initialization
class IntelligentTestDouble:
"""
Intelligent Test Double with LangGraph workflow integration.
Features:
- All test double types (dummy, fake, spy, mock, stub)
- LLM-powered behavior analysis
- Expectation verification
- Call recording and replay
- Pattern detection
- Intelligent recommendations
"""
Explanation: Main class implementing all 5 double types with AI analysis.
def __init__(self, name: str, double_type: DoubleType, model_name: str = "gpt-4", temperature: float = 0.5):
self.name = name
self.double_type = double_type
self.llm = ChatOpenAI(model=model_name, temperature=temperature)
# Configuration
self.methods: Dict[str, Any] = {}
self.expectations: Dict[str, List[Expectation]] = defaultdict(list)
self.fake_implementations: Dict[str, Callable] = {}
Explanation: Initialize with: – Name and type of double – LLM for analysis – Method configurations – Mock expectations – Fake implementations (actual functions)
# Tracking
self.call_records: List[CallRecord] = []
self.call_counts: Dict[str, int] = defaultdict(int)
# Build workflow
self.workflow = self._build_workflow()
# Statistics
self.stats = {
"total_calls": 0,
"verifications_passed": 0,
"verifications_failed": 0,
"double_type": double_type.value
}
Explanation: Initialize call tracking, workflow, and statistics.
Lines 139-157: Workflow Building
def _build_workflow(self) -> StateGraph:
"""Build LangGraph workflow."""
workflow = StateGraph(TestDoubleState)
workflow.add_node("analyze_double_type", self._analyze_double_type_node)
workflow.add_node("configure_behavior", self._configure_behavior_node)
workflow.add_node("execute_call", self._execute_call_node)
workflow.add_node("verify_expectations", self._verify_expectations_node)
workflow.add_node("generate_report", self._generate_report_node)
Explanation: Create 5-node workflow: 1. Analyze what type of double is needed 2. Configure behavior based on type 3. Execute the call 4. Verify expectations (for mocks) 5. Generate report
workflow.set_entry_point("analyze_double_type")
workflow.add_edge("analyze_double_type", "configure_behavior")
workflow.add_edge("configure_behavior", "execute_call")
workflow.add_edge("execute_call", "verify_expectations")
workflow.add_edge("verify_expectations", "generate_report")
workflow.add_edge("generate_report", END)
return workflow.compile()
Explanation: Define linear flow through all nodes.
Lines 159-216: Analyze Double Type Node (LLM-Powered)
def _analyze_double_type_node(self, state: TestDoubleState) -> TestDoubleState:
"""Analyze test double type and usage."""
method_name = state["method_name"]
args = state["args"]
kwargs = state["kwargs"]
# Increment call count
self.call_counts[method_name] += 1
state["call_count"] = self.call_counts[method_name]
Explanation: Extract parameters and update call counter.
# Use LLM to analyze if this is first call to method
if self.call_counts[method_name] == 1 and not self.methods.get(method_name):
prompt = ChatPromptTemplate.from_messages([
("system", "You are an expert in test double patterns and testing strategies."),
("user", """Analyze this method call for test double usage:
Method: {method_name}
Arguments: {args}
Keyword Arguments: {kwargs}
Current Double Type: {double_type}
Provide analysis as JSON with:
- recommended_type: Best double type for this method
- reasoning: Why this type is recommended
- complexity_score: 0.0-1.0 score for implementation complexity
- usage_patterns: List of observed usage patterns
Double types: dummy (parameter filler), fake (simplified working), spy (record calls), mock (verify expectations), stub (fixed responses)""")
])
Explanation: On first call, ask LLM to analyze method and recommend best double type.
chain = prompt | self.llm
response = chain.invoke({
"method_name": method_name,
"args": str(args),
"kwargs": str(kwargs),
"double_type": self.double_type.value
})
try:
analysis_data = json.loads(response.content)
state["analysis"] = DoubleAnalysis(
recommended_type=DoubleType(analysis_data.get("recommended_type", self.double_type.value)),
reasoning=analysis_data.get("reasoning", "Default reasoning"),
complexity_score=analysis_data.get("complexity_score", 0.5),
usage_patterns=analysis_data.get("usage_patterns", [])
)
except:
state["analysis"] = DoubleAnalysis(
recommended_type=self.double_type,
reasoning="Using configured double type",
complexity_score=0.5,
usage_patterns=[]
)
Explanation: Invoke LLM and parse JSON response into analysis object. Fallback to current type if parsing fails.
return state
Lines 218-230: Configure Behavior Node
def _configure_behavior_node(self, state: TestDoubleState) -> TestDoubleState:
"""Configure double behavior."""
method_name = state["method_name"]
# Store method configuration if not exists
if method_name not in self.methods:
self.methods[method_name] = {
"return_value": None,
"configured": False
}
return state
Explanation: Ensure method has configuration entry. Simple node that prepares for execution.
Lines 232-291: Execute Call Node
def _execute_call_node(self, state: TestDoubleState) -> TestDoubleState:
"""Execute the test double call."""
method_name = state["method_name"]
args = state["args"]
kwargs = state["kwargs"]
double_type = state["double_type"]
result = None
Explanation: Extract parameters and initialize result.
if double_type == DoubleType.DUMMY:
# Dummy: Do nothing, return None
result = None
Explanation: DUMMY – simplest double, does nothing. Used when object is required but never actually used.
elif double_type == DoubleType.STUB:
# Stub: Return configured value
method_config = self.methods.get(method_name, {})
result = method_config.get("return_value")
Explanation: STUB – returns pre-configured value. No logic, just returns what you told it to.
elif double_type == DoubleType.FAKE:
# Fake: Use simplified implementation
if method_name in self.fake_implementations:
try:
result = self.fake_implementations[method_name](*args, **kwargs)
except Exception as e:
result = {"error": str(e)}
else:
# Default fake behavior
result = {"status": "fake_success", "method": method_name, "data": "fake_data"}
Explanation: FAKE – has working implementation, but simplified (e.g., in-memory instead of real database). Actually executes logic.
elif double_type == DoubleType.SPY:
# Spy: Record call and execute real implementation if available
if method_name in self.fake_implementations:
try:
result = self.fake_implementations[method_name](*args, **kwargs)
except Exception as e:
result = {"error": str(e)}
else:
result = {"status": "spy_recorded", "method": method_name}
Explanation: SPY – like fake but emphasizes call recording. Executes real code while tracking calls.
elif double_type == DoubleType.MOCK:
# Mock: Return expected value if configured
expectations = self.expectations.get(method_name, [])
if expectations:
# Use first expectation's return value
result = expectations[0].return_value
else:
result = {"status": "mock_success", "method": method_name}
Explanation: MOCK – returns value from expectations. Unlike stub, mock will be verified later.
state["result"] = result
# Record call
call_record = CallRecord(
method=method_name,
args=args,
kwargs=kwargs,
result=result,
double_type=double_type
)
self.call_records.append(call_record)
self.stats["total_calls"] += 1
return state
Explanation: Store result, record call in history, update statistics.
Lines 293-346: Verify Expectations Node
def _verify_expectations_node(self, state: TestDoubleState) -> TestDoubleState:
"""Verify expectations for mocks."""
method_name = state["method_name"]
args = state["args"]
kwargs = state["kwargs"]
expectations_met = True
Explanation: Initialize verification – only meaningful for mocks.
if state["double_type"] == DoubleType.MOCK:
expectations = self.expectations.get(method_name, [])
for expectation in expectations:
# Check call count
actual_calls = self.call_counts[method_name]
if actual_calls != expectation.expected_calls:
expectations_met = False
state["insights"] = state.get("insights", [])
state["insights"].append(
f"Expected {expectation.expected_calls} calls to {method_name}, got {actual_calls}"
)
Explanation: Verify mock was called expected number of times. This is the key difference between mocks and stubs – mocks verify behavior.
# Check arguments if specified
if expectation.expected_args is not None and args != expectation.expected_args:
expectations_met = False
state["insights"] = state.get("insights", [])
state["insights"].append(
f"Expected args {expectation.expected_args}, got {args}"
)
if expectation.expected_kwargs is not None and kwargs != expectation.expected_kwargs:
expectations_met = False
state["insights"] = state.get("insights", [])
state["insights"].append(
f"Expected kwargs {expectation.expected_kwargs}, got {kwargs}"
)
Explanation: Verify arguments match expectations if specified.
state["expectations_met"] = expectations_met
state["verification_status"] = VerificationStatus.PASSED if expectations_met else VerificationStatus.FAILED
if expectations_met:
self.stats["verifications_passed"] += 1
else:
self.stats["verifications_failed"] += 1
return state
Explanation: Set verification status and update statistics.
Lines 348-361: Generate Report Node
def _generate_report_node(self, state: TestDoubleState) -> TestDoubleState:
"""Generate insights report."""
if not state.get("insights"):
state["insights"] = []
# Add summary insight
state["insights"].append(
f"Test double '{self.name}' of type {state['double_type'].value} executed {state['method_name']}"
)
return state
Explanation: Add summary insight to state. Simple reporting node.
Lines 363-403: Public Configuration Methods
def configure_method(
self,
method_name: str,
return_value: Any = None,
implementation: Optional[Callable] = None
):
"""Configure method behavior."""
self.methods[method_name] = {
"return_value": return_value,
"configured": True
}
if implementation:
self.fake_implementations[method_name] = implementation
Explanation: Configure what method should return (for stubs) or how it should behave (for fakes – actual function).
def expect_call(
self,
method_name: str,
times: int = 1,
with_args: Optional[tuple] = None,
with_kwargs: Optional[Dict[str, Any]] = None,
returns: Any = None
):
"""Set expectation for mock."""
expectation = Expectation(
method=method_name,
expected_calls=times,
expected_args=with_args,
expected_kwargs=with_kwargs,
return_value=returns
)
self.expectations[method_name].append(expectation)
Explanation: Set expectations for mocks. Used in test setup phase. Example: mock.expect_call("save", times=1, with_args=("data",)).
def call_method(self, method_name: str, *args, **kwargs) -> Any:
"""Execute method call through workflow."""
initial_state: TestDoubleState = {
"method_name": method_name,
"args": args,
"kwargs": kwargs,
"double_type": self.double_type,
"analysis": None,
"result": None,
"verification_status": VerificationStatus.NOT_VERIFIED,
"expectations_met": True,
"insights": [],
"call_count": 0
}
final_state = self.workflow.invoke(initial_state)
return final_state["result"]
Explanation: Main entry point – execute workflow and return result.
Lines 428-475: TestDoublePattern Coordinator
class TestDoublePattern:
"""
Comprehensive Test Double Pattern coordinator with LangChain/LangGraph.
"""
def __init__(self, model_name: str = "gpt-4"):
self.doubles: Dict[str, IntelligentTestDouble] = {}
self.model_name = model_name
Explanation: Coordinator managing multiple test doubles.
def create_dummy(self, name: str) -> IntelligentTestDouble:
"""Create a dummy double."""
double = IntelligentTestDouble(name, DoubleType.DUMMY, model_name=self.model_name)
self.doubles[name] = double
return double
Explanation: Factory methods for each double type. Clear, explicit API.
Similar factory methods for stub, fake, spy, and mock types follow the same pattern.
Lines 477-675: Demo Code
Scenario 1: Dummy Double
dummy = pattern.create_dummy("DummyLogger")
result1 = pattern.call("DummyLogger", "log", message="Test message")
Explanation: Dummy does nothing, returns None. Used when parameter is required but not used (e.g., logger in test).
Scenario 2: Stub Double
stub = pattern.create_stub("DatabaseStub")
stub.configure_method("get_user", return_value={"id": 1, "name": "Test User", "email": "test@example.com"})
Explanation: Stub returns fixed value. Good for tests that just need data, don’t care about interactions.
Scenario 3: Fake Double
fake = pattern.create_fake("FakeEmailService")
def fake_send_email(to: str, subject: str, body: str = "") -> Dict[str, Any]:
return {
"status": "sent",
"message_id": f"FAKE_{hash(to + subject)}",
"to": to,
"subject": subject
}
fake.configure_method("send_email", implementation=fake_send_email)
Explanation: Fake has working implementation (simplified). Actually processes inputs, but doesn’t hit real email server.
Scenario 4: Spy Double
spy = pattern.create_spy("MetricsSpy")
def track_metric(metric_name: str, value: float) -> Dict[str, Any]:
return {
"tracked": True,
"metric": metric_name,
"value": value,
"timestamp": datetime.now().isoformat()
}
spy.configure_method("track", implementation=track_metric)
# Later, verify calls were made
calls = spy.get_calls("track")
for i, call in enumerate(calls, 1):
print(f" Call {i}: args={call.args}, kwargs={call.kwargs}")
Explanation: Spy executes real code AND records calls. Used to verify side effects.
Scenario 5: Mock Double
mock = pattern.create_mock("PaymentMock")
# Set expectations
mock.expect_call(
"process_payment",
times=2,
returns={"status": "success", "transaction_id": "TXN123"}
)
# Call twice
result8 = pattern.call("PaymentMock", "process_payment", amount=100.0)
result9 = pattern.call("PaymentMock", "process_payment", amount=50.0)
# Verify expectations
print(f"\nVerifying 'process_payment' called 2 times: {mock.verify('process_payment', times=2)}")
Explanation: Mock has expectations that must be met. Test fails if method not called expected times. Key difference from stub – verification is mandatory.
Test Double Comparison
| Double | Purpose | Has Logic? | Verifies Behavior? | Example |
|---|---|---|---|---|
| Dummy | Fill parameter | No | No | Null logger |
| Stub | Provide data | No | No | Return fixed user |
| Fake | Working impl | Yes | No | In-memory DB |
| Spy | Record calls | Yes | Optional | Track metrics |
| Mock | Verify expectations | No | Yes | Assert called twice |
When to Use Each
- Dummy: Parameter required but not used (null object pattern)
- Stub: Need data, don’t care about interactions
- Fake: Need working implementation but real one is too slow/complex
- Spy: Need to verify side effects while using real code
- Mock: Need to verify specific interactions occurred
Key Design Patterns
- Strategy Pattern: Different double types implement different strategies
- Template Method: Common workflow with type-specific execution
- Observer Pattern: Call recording observes all invocations
- Proxy Pattern: Doubles proxy real objects
- Factory Pattern: Coordinator creates appropriate doubles
Performance Considerations
- Dummy/Stub/Mock: O(1) – instant
- Fake/Spy: O(implementation) – depends on fake logic
- Verification: O(calls × expectations) – linear in recorded calls
- Memory: O(calls) – stores all call records
Extension Points
- Add partial mocks (some methods real, some mocked)
- Implement argument matchers (any(), contains(), etc.)
- Add sequence verification (call A before call B)
- Implement call chains for fluent APIs
- Add async/await support for async methods