"""
Pattern 266: Stub Pattern with LangChain/LangGraph
Purpose: Minimal test implementations with controlled, predetermined responses.
Provides intelligent stub selection and response generation with LLM 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 StubType(Enum):
"""Stub implementation types."""
SIMPLE = "simple" # Return fixed value
SEQUENCE = "sequence" # Return values in sequence
CONDITIONAL = "conditional" # Return based on input
EXCEPTION = "exception" # Raise exception
NULL = "null" # Return None
DYNAMIC = "dynamic" # LLM-generated response
class StubComplexity(Enum):
"""Complexity of stub response."""
TRIVIAL = "trivial"
SIMPLE = "simple"
MODERATE = "moderate"
COMPLEX = "complex"
@dataclass
class StubResponse:
"""Stub response configuration."""
method_name: str
stub_type: StubType
return_value: Any = None
return_sequence: List[Any] = field(default_factory=list)
condition: Optional[Callable] = None
exception: Optional[Exception] = None
complexity: StubComplexity = StubComplexity.SIMPLE
@dataclass
class StubCall:
"""Record of stub call."""
method_name: str
args: tuple
kwargs: Dict[str, Any]
result: Any
stub_type: StubType
call_number: int
timestamp: datetime = field(default_factory=datetime.now)
@dataclass
class StubAnalysis:
"""Analysis of stub behavior."""
recommended_type: StubType
complexity: StubComplexity
reasoning: str
alternative_approaches: List[str]
class StubState(TypedDict):
"""State for stub workflow."""
method_name: str
args: tuple
kwargs: Dict[str, Any]
stub_config: Optional[StubResponse]
recommended_type: Optional[StubType]
complexity: Optional[StubComplexity]
analysis: Optional[StubAnalysis]
result: Any
validation_passed: bool
insights: List[str]
call_number: int
class IntelligentStubManager:
"""
Intelligent Stub Manager with LangGraph workflow integration.
Features:
- Intelligent stub type selection
- LLM-powered response generation
- Dynamic stub behavior
- Call analysis and insights
- Complexity assessment
- Pattern detection
"""
def __init__(self, name: str, model_name: str = "gpt-4", temperature: float = 0.5):
self.name = name
self.llm = ChatOpenAI(model=model_name, temperature=temperature)
# Stub configurations
self.stubs: Dict[str, StubResponse] = {}
# Call history
self.call_history: List[StubCall] = []
self.call_counts: Dict[str, int] = defaultdict(int)
self.sequence_indices: Dict[str, int] = defaultdict(int)
# Build workflow
self.workflow = self._build_workflow()
# Statistics
self.stats = {
"total_calls": 0,
"stub_types": {t.value: 0 for t in StubType},
"methods_stubbed": 0
}
def _build_workflow(self) -> StateGraph:
"""Build LangGraph workflow."""
workflow = StateGraph(StubState)
workflow.add_node("analyze_request", self._analyze_request_node)
workflow.add_node("select_stub_type", self._select_stub_type_node)
workflow.add_node("generate_response", self._generate_response_node)
workflow.add_node("validate_response", self._validate_response_node)
workflow.add_node("record_call", self._record_call_node)
workflow.set_entry_point("analyze_request")
workflow.add_edge("analyze_request", "select_stub_type")
workflow.add_edge("select_stub_type", "generate_response")
workflow.add_edge("generate_response", "validate_response")
workflow.add_edge("validate_response", "record_call")
workflow.add_edge("record_call", END)
return workflow.compile()
def _analyze_request_node(self, state: StubState) -> StubState:
"""Analyze the stub request."""
method_name = state["method_name"]
args = state["args"]
kwargs = state["kwargs"]
# Get stub configuration if exists
stub_config = self.stubs.get(method_name)
state["stub_config"] = stub_config
# Increment call count
self.call_counts[method_name] += 1
state["call_number"] = self.call_counts[method_name]
# If no stub configured, use LLM to analyze
if not stub_config:
prompt = ChatPromptTemplate.from_messages([
("system", "You are an expert in test stub design and testing patterns."),
("user", """Analyze this method call and recommend an appropriate stub type:
Method: {method_name}
Arguments: {args}
Keyword Arguments: {kwargs}
Recommend:
1. Best stub type (simple, sequence, conditional, dynamic, null, exception)
2. Complexity level (trivial, simple, moderate, complex)
3. Reasoning for recommendation
4. Alternative approaches
Provide response as JSON with keys: recommended_type, complexity, reasoning, alternatives""")
])
chain = prompt | self.llm
response = chain.invoke({
"method_name": method_name,
"args": str(args),
"kwargs": str(kwargs)
})
try:
analysis_data = json.loads(response.content)
state["analysis"] = StubAnalysis(
recommended_type=StubType(analysis_data.get("recommended_type", "simple")),
complexity=StubComplexity(analysis_data.get("complexity", "simple")),
reasoning=analysis_data.get("reasoning", "No reasoning provided"),
alternative_approaches=analysis_data.get("alternatives", [])
)
except:
# Fallback
state["analysis"] = StubAnalysis(
recommended_type=StubType.SIMPLE,
complexity=StubComplexity.SIMPLE,
reasoning="Default simple stub",
alternative_approaches=[]
)
return state
def _select_stub_type_node(self, state: StubState) -> StubState:
"""Select appropriate stub type."""
stub_config = state["stub_config"]
if stub_config:
# Use configured stub type
state["recommended_type"] = stub_config.stub_type
state["complexity"] = stub_config.complexity
else:
# Use analyzed recommendation
analysis = state.get("analysis")
if analysis:
state["recommended_type"] = analysis.recommended_type
state["complexity"] = analysis.complexity
else:
state["recommended_type"] = StubType.SIMPLE
state["complexity"] = StubComplexity.SIMPLE
return state
def _generate_response_node(self, state: StubState) -> StubState:
"""Generate stub response."""
stub_type = state["recommended_type"]
stub_config = state["stub_config"]
method_name = state["method_name"]
args = state["args"]
kwargs = state["kwargs"]
result = None
try:
if stub_type == StubType.EXCEPTION:
if stub_config and stub_config.exception:
raise stub_config.exception
else:
raise Exception(f"Stub exception for {method_name}")
elif stub_type == StubType.NULL:
result = None
elif stub_type == StubType.SIMPLE:
if stub_config:
result = stub_config.return_value
else:
result = {"status": "stubbed", "method": method_name}
elif stub_type == StubType.SEQUENCE:
if stub_config and stub_config.return_sequence:
idx = self.sequence_indices[method_name]
if idx StubState:
"""Validate generated response."""
result = state["result"]
stub_type = state["recommended_type"]
# Basic validation
validation_passed = True
if stub_type == StubType.NULL:
validation_passed = result is None
elif stub_type == StubType.EXCEPTION:
validation_passed = isinstance(result, dict) and "error" in result
elif stub_type == StubType.SIMPLE:
validation_passed = result is not None
state["validation_passed"] = validation_passed
if not validation_passed:
state["insights"] = state.get("insights", [])
state["insights"].append(f"Validation failed for {stub_type.value} stub")
return state
def _record_call_node(self, state: StubState) -> StubState:
"""Record the stub call."""
call = StubCall(
method_name=state["method_name"],
args=state["args"],
kwargs=state["kwargs"],
result=state["result"],
stub_type=state["recommended_type"],
call_number=state["call_number"]
)
self.call_history.append(call)
self.stats["total_calls"] += 1
self.stats["stub_types"][state["recommended_type"].value] += 1
return state
def register_stub(
self,
method_name: str,
stub_type: StubType = StubType.SIMPLE,
return_value: Any = None,
return_sequence: List[Any] = None,
condition: Optional[Callable] = None,
exception: Optional[Exception] = None,
complexity: StubComplexity = StubComplexity.SIMPLE
):
"""Register a stub configuration."""
stub = StubResponse(
method_name=method_name,
stub_type=stub_type,
return_value=return_value,
return_sequence=return_sequence or [],
condition=condition,
exception=exception,
complexity=complexity
)
self.stubs[method_name] = stub
self.stats["methods_stubbed"] = len(self.stubs)
def call(self, method_name: str, *args, **kwargs) -> Any:
"""Execute stub method call through workflow."""
initial_state: StubState = {
"method_name": method_name,
"args": args,
"kwargs": kwargs,
"stub_config": None,
"recommended_type": None,
"complexity": None,
"analysis": None,
"result": None,
"validation_passed": False,
"insights": [],
"call_number": 0
}
final_state = self.workflow.invoke(initial_state)
return final_state["result"]
def get_call_history(self, method_name: str = None) -> List[StubCall]:
"""Get call history."""
if method_name:
return [call for call in self.call_history if call.method_name == method_name]
return self.call_history
def get_statistics(self) -> Dict[str, Any]:
"""Get stub statistics."""
return {
"name": self.name,
**self.stats,
"unique_methods": len(set(call.method_name for call in self.call_history))
}
def reset(self):
"""Reset stub state."""
self.call_history.clear()
self.call_counts.clear()
self.sequence_indices.clear()
self.stats["total_calls"] = 0
self.stats["stub_types"] = {t.value: 0 for t in StubType}
class StubPattern:
"""
Comprehensive Stub Pattern coordinator with LangChain/LangGraph.
"""
def __init__(self, model_name: str = "gpt-4"):
self.managers: Dict[str, IntelligentStubManager] = {}
self.model_name = model_name
def create_manager(self, name: str, target_class: str = "") -> IntelligentStubManager:
"""Create a stub manager."""
manager = IntelligentStubManager(name, model_name=self.model_name)
self.managers[name] = manager
return manager
def call(self, manager_name: str, method_name: str, *args, **kwargs) -> Dict[str, Any]:
"""Execute stub call."""
if manager_name not in self.managers:
return {"error": f"Manager {manager_name} not found"}
manager = self.managers[manager_name]
result = manager.call(method_name, *args, **kwargs)
return {
"manager": manager_name,
"method": method_name,
"result": result,
"call_count": manager.call_counts[method_name]
}
def get_statistics(self) -> Dict[str, Any]:
"""Get overall statistics."""
total_calls = sum(m.stats["total_calls"] for m in self.managers.values())
total_methods = sum(m.stats["methods_stubbed"] for m in self.managers.values())
return {
"total_managers": len(self.managers),
"total_calls": total_calls,
"total_methods_stubbed": total_methods
}
if __name__ == "__main__":
print("=== Stub Pattern with LangChain/LangGraph Demo ===\n")
print("Scenario 1: Database Stub with Sequence")
print("-" * 50)
pattern = StubPattern(model_name="gpt-4")
db_manager = pattern.create_manager("DatabaseStub", "Database")
# Register sequence stub
db_manager.register_stub(
"get_users",
stub_type=StubType.SEQUENCE,
return_sequence=[
[{"id": 1, "name": "Alice"}],
[{"id": 2, "name": "Bob"}],
[{"id": 3, "name": "Charlie"}]
],
complexity=StubComplexity.SIMPLE
)
print("\nExecuting sequence stub calls...")
for i in range(3):
result = pattern.call("DatabaseStub", "get_users")
print(f"Call {i+1}: {result['result']}")
print("\n" + "=" * 50)
print("\nScenario 2: API Stub with Conditional Logic")
print("-" * 50)
api_manager = pattern.create_manager("APIStub", "ExternalAPI")
# Register conditional stub
api_manager.register_stub(
"fetch_data",
stub_type=StubType.CONDITIONAL,
return_value={"status": "success", "data": "test_data"},
condition=lambda *args, **kwargs: kwargs.get("authenticated", False),
complexity=StubComplexity.MODERATE
)
print("\nAuthenticated call:")
result1 = pattern.call("APIStub", "fetch_data", authenticated=True)
print(f"Result: {result1['result']}")
print("\nUnauthenticated call:")
result2 = pattern.call("APIStub", "fetch_data", authenticated=False)
print(f"Result: {result2['result']}")
print("\n" + "=" * 50)
print("\nScenario 3: Dynamic Stub with LLM Generation")
print("-" * 50)
service_manager = pattern.create_manager("ServiceStub", "PaymentService")
# Register dynamic stub (no configuration - uses LLM)
print("\nCalling unconfigured method (LLM will generate response)...")
result3 = pattern.call("ServiceStub", "process_payment",
amount=100.0, currency="USD", user_id=123)
print(f"Result: {result3['result']}")
print("\n" + "=" * 50)
print("\nScenario 4: Exception Stub")
print("-" * 50)
error_manager = pattern.create_manager("ErrorStub", "FailingService")
# Register exception stub
error_manager.register_stub(
"dangerous_operation",
stub_type=StubType.EXCEPTION,
exception=Exception("Operation not allowed in test mode"),
complexity=StubComplexity.TRIVIAL
)
print("\nCalling exception stub...")
result4 = pattern.call("ErrorStub", "dangerous_operation")
print(f"Result: {result4['result']}")
print("\n" + "=" * 50)
print("\nScenario 5: Simple Fixed Response Stub")
print("-" * 50)
config_manager = pattern.create_manager("ConfigStub", "ConfigService")
# Register simple stub
config_manager.register_stub(
"get_config",
stub_type=StubType.SIMPLE,
return_value={"timeout": 30, "retries": 3, "debug": True},
complexity=StubComplexity.TRIVIAL
)
print("\nCalling simple stub...")
result5 = pattern.call("ConfigStub", "get_config")
print(f"Result: {result5['result']}")
print("\n" + "=" * 50)
print("\nCall History Analysis")
print("-" * 50)
# Show call history for database stub
db_history = db_manager.get_call_history("get_users")
print(f"\nDatabase 'get_users' call history ({len(db_history)} calls):")
for call in db_history:
print(f" Call #{call.call_number}: Type={call.stub_type.value}, Result={call.result}")
print("\n" + "=" * 50)
print("\nStatistics")
print("-" * 50)
overall_stats = pattern.get_statistics()
print(f"Overall Statistics:")
print(f" Total Managers: {overall_stats['total_managers']}")
print(f" Total Calls: {overall_stats['total_calls']}")
print(f" Total Methods Stubbed: {overall_stats['total_methods_stubbed']}")
print(f"\nDatabase Stub Statistics:")
db_stats = db_manager.get_statistics()
for key, value in db_stats.items():
print(f" {key}: {value}")
print(f"\nAPI Stub Statistics:")
api_stats = api_manager.get_statistics()
for key, value in api_stats.items():
print(f" {key}: {value}")
print("\n" + "=" * 50)
print("Stub pattern demonstration complete!")
print("=" * 50)
Agentic Architecture Pattern: Stub
Pattern 266: Stub Pattern – Line-by-Line Explanation
Overview
This pattern implements minimal test implementations with controlled, predetermined responses. It provides intelligent stub selection and response generation with LLM analysis for testing without real dependencies.
Code Explanation
Lines 1-17: Imports and Setup
"""
Pattern 266: Stub Pattern with LangChain/LangGraph
Purpose: Minimal test implementations with controlled, predetermined responses.
Provides intelligent stub selection and response generation with LLM analysis.
"""
Explanation: Module docstring describing the pattern’s purpose – creating test stubs that replace real implementations with controlled, predictable responses.
from typing import Dict, Any, List, Optional, Callable, TypedDict
Explanation: Import type hints. Callable is used for function types (validators, conditions).
from dataclasses import dataclass, field
from datetime import datetime
from enum import Enum
from collections import defaultdict
Explanation: Import dataclasses, datetime for timestamps, Enum for type-safe enumerations, and defaultdict for automatic dictionary initialization.
from langchain_core.prompts import ChatPromptTemplate
from langchain_openai import ChatOpenAI
from langgraph.graph import StateGraph, END
import json
Explanation: Import LangChain/LangGraph components and json for parsing LLM responses.
Lines 19-32: Enum Definitions
class StubType(Enum):
"""Stub implementation types."""
SIMPLE = "simple" # Return fixed value
SEQUENCE = "sequence" # Return values in sequence
CONDITIONAL = "conditional" # Return based on input
EXCEPTION = "exception" # Raise exception
NULL = "null" # Return None
DYNAMIC = "dynamic" # LLM-generated response
Explanation: Enum defining 6 stub types: – SIMPLE: Always returns same fixed value – SEQUENCE: Returns different values on successive calls – CONDITIONAL: Returns value based on input conditions – EXCEPTION: Simulates error by raising exception – NULL: Always returns None (for optional parameters) – DYNAMIC: Uses LLM to generate realistic responses
class StubComplexity(Enum):
"""Complexity of stub response."""
TRIVIAL = "trivial"
SIMPLE = "simple"
MODERATE = "moderate"
COMPLEX = "complex"
Explanation: Enum classifying stub implementation complexity from trivial (basic return) to complex (conditional logic).
Lines 34-62: Dataclasses
@dataclass
class StubResponse:
"""Stub response configuration."""
method_name: str
stub_type: StubType
return_value: Any = None
return_sequence: List[Any] = field(default_factory=list)
condition: Optional[Callable] = None
exception: Optional[Exception] = None
complexity: StubComplexity = StubComplexity.SIMPLE
Explanation: Configuration for a stub method:
– method_name: Method being stubbed (e.g., “get_user”)
– stub_type: Type from StubType enum
– return_value: Value to return (for SIMPLE/CONDITIONAL)
– return_sequence: List of values (for SEQUENCE)
– condition: Function to evaluate (for CONDITIONAL)
– exception: Exception to raise (for EXCEPTION)
– complexity: Implementation complexity level
@dataclass
class StubCall:
"""Record of stub call."""
method_name: str
args: tuple
kwargs: Dict[str, Any]
result: Any
stub_type: StubType
call_number: int
timestamp: datetime = field(default_factory=datetime.now)
Explanation: Record of each stub invocation with: – Arguments passed – Result returned – Type of stub used – Call sequence number – Timestamp for tracking
@dataclass
class StubAnalysis:
"""Analysis of stub behavior."""
recommended_type: StubType
complexity: StubComplexity
reasoning: str
alternative_approaches: List[str]
Explanation: LLM-generated analysis recommending best stub type, complexity assessment, reasoning, and alternative approaches.
Lines 64-77: TypedDict State
class StubState(TypedDict):
"""State for stub workflow."""
method_name: str
args: tuple
kwargs: Dict[str, Any]
stub_config: Optional[StubResponse]
recommended_type: Optional[StubType]
complexity: Optional[StubComplexity]
analysis: Optional[StubAnalysis]
result: Any
validation_passed: bool
insights: List[str]
call_number: int
Explanation: Workflow state containing: – Method call details (name, args, kwargs) – Configuration and recommendations – Analysis results – Generated result – Validation status – Call tracking
Lines 79-129: IntelligentStubManager Initialization
class IntelligentStubManager:
"""
Intelligent Stub Manager with LangGraph workflow integration.
Features:
- Intelligent stub type selection
- LLM-powered response generation
- Dynamic stub behavior
- Call analysis and insights
- Complexity assessment
- Pattern detection
"""
Explanation: Main class for managing stubs with AI-powered selection and generation.
def __init__(self, name: str, model_name: str = "gpt-4", temperature: float = 0.5):
self.name = name
self.llm = ChatOpenAI(model=model_name, temperature=temperature)
# Stub configurations
self.stubs: Dict[str, StubResponse] = {}
# Call history
self.call_history: List[StubCall] = []
self.call_counts: Dict[str, int] = defaultdict(int)
self.sequence_indices: Dict[str, int] = defaultdict(int)
Explanation: Initialize with: – Name for identification – LLM for intelligent analysis – Stub configurations dictionary – Call tracking (history, counts, sequence indices)
# Build workflow
self.workflow = self._build_workflow()
# Statistics
self.stats = {
"total_calls": 0,
"stub_types": {t.value: 0 for t in StubType},
"methods_stubbed": 0
}
Explanation: Build workflow and initialize statistics tracking.
Lines 131-149: Workflow Building
def _build_workflow(self) -> StateGraph:
"""Build LangGraph workflow."""
workflow = StateGraph(StubState)
workflow.add_node("analyze_request", self._analyze_request_node)
workflow.add_node("select_stub_type", self._select_stub_type_node)
workflow.add_node("generate_response", self._generate_response_node)
workflow.add_node("validate_response", self._validate_response_node)
workflow.add_node("record_call", self._record_call_node)
Explanation: Create 5-node workflow for processing stub calls.
workflow.set_entry_point("analyze_request")
workflow.add_edge("analyze_request", "select_stub_type")
workflow.add_edge("select_stub_type", "generate_response")
workflow.add_edge("generate_response", "validate_response")
workflow.add_edge("validate_response", "record_call")
workflow.add_edge("record_call", END)
return workflow.compile()
Explanation: Define linear flow: analyze → select → generate → validate → record → end.
Lines 151-206: Analyze Request Node (LLM-Powered)
def _analyze_request_node(self, state: StubState) -> StubState:
"""Analyze the stub request."""
method_name = state["method_name"]
args = state["args"]
kwargs = state["kwargs"]
# Get stub configuration if exists
stub_config = self.stubs.get(method_name)
state["stub_config"] = stub_config
# Increment call count
self.call_counts[method_name] += 1
state["call_number"] = self.call_counts[method_name]
Explanation: Extract call details, retrieve any existing configuration, update call count.
# If no stub configured, use LLM to analyze
if not stub_config:
prompt = ChatPromptTemplate.from_messages([
("system", "You are an expert in test stub design and testing patterns."),
("user", """Analyze this method call and recommend an appropriate stub type:
Method: {method_name}
Arguments: {args}
Keyword Arguments: {kwargs}
Recommend:
1. Best stub type (simple, sequence, conditional, dynamic, null, exception)
2. Complexity level (trivial, simple, moderate, complex)
3. Reasoning for recommendation
4. Alternative approaches
Provide response as JSON with keys: recommended_type, complexity, reasoning, alternatives""")
])
Explanation: If method not configured, create prompt asking LLM to analyze the call and recommend stub type.
chain = prompt | self.llm
response = chain.invoke({
"method_name": method_name,
"args": str(args),
"kwargs": str(kwargs)
})
Explanation: Invoke LLM with method details.
try:
analysis_data = json.loads(response.content)
state["analysis"] = StubAnalysis(
recommended_type=StubType(analysis_data.get("recommended_type", "simple")),
complexity=StubComplexity(analysis_data.get("complexity", "simple")),
reasoning=analysis_data.get("reasoning", "No reasoning provided"),
alternative_approaches=analysis_data.get("alternatives", [])
)
except:
# Fallback
state["analysis"] = StubAnalysis(
recommended_type=StubType.SIMPLE,
complexity=StubComplexity.SIMPLE,
reasoning="Default simple stub",
alternative_approaches=[]
)
Explanation: Parse JSON response into StubAnalysis object. If parsing fails, use simple stub as fallback.
return state
Lines 208-226: Select Stub Type Node
def _select_stub_type_node(self, state: StubState) -> StubState:
"""Select appropriate stub type."""
stub_config = state["stub_config"]
if stub_config:
# Use configured stub type
state["recommended_type"] = stub_config.stub_type
state["complexity"] = stub_config.complexity
else:
# Use analyzed recommendation
analysis = state.get("analysis")
if analysis:
state["recommended_type"] = analysis.recommended_type
state["complexity"] = analysis.complexity
else:
state["recommended_type"] = StubType.SIMPLE
state["complexity"] = StubComplexity.SIMPLE
return state
Explanation: Decide stub type: use configured type if available, otherwise use LLM recommendation, or default to SIMPLE.
Lines 228-310: Generate Response Node
def _generate_response_node(self, state: StubState) -> StubState:
"""Generate stub response."""
stub_type = state["recommended_type"]
stub_config = state["stub_config"]
method_name = state["method_name"]
args = state["args"]
kwargs = state["kwargs"]
result = None
Explanation: Extract parameters and initialize result.
try:
if stub_type == StubType.EXCEPTION:
if stub_config and stub_config.exception:
raise stub_config.exception
else:
raise Exception(f"Stub exception for {method_name}")
Explanation: EXCEPTION type: raise configured exception or generic exception.
elif stub_type == StubType.NULL:
result = None
Explanation: NULL type: always return None.
elif stub_type == StubType.SIMPLE:
if stub_config:
result = stub_config.return_value
else:
result = {"status": "stubbed", "method": method_name}
Explanation: SIMPLE type: return configured value or generic stub response.
elif stub_type == StubType.SEQUENCE:
if stub_config and stub_config.return_sequence:
idx = self.sequence_indices[method_name]
if idx < len(stub_config.return_sequence):
result = stub_config.return_sequence[idx]
self.sequence_indices[method_name] += 1
else:
result = stub_config.return_sequence[-1] if stub_config.return_sequence else None
else:
result = {"status": "sequence_stub", "index": self.sequence_indices[method_name]}
self.sequence_indices[method_name] += 1
Explanation: SEQUENCE type: return next value in sequence, track index. After sequence exhausted, return last value.
elif stub_type == StubType.CONDITIONAL:
if stub_config and stub_config.condition:
if stub_config.condition(*args, **kwargs):
result = stub_config.return_value
else:
result = None
else:
# Default conditional behavior
result = {"status": "conditional", "condition_met": True}
Explanation: CONDITIONAL type: evaluate condition function with arguments, return value if true, None if false.
elif stub_type == StubType.DYNAMIC:
# Use LLM to generate dynamic response
prompt = ChatPromptTemplate.from_messages([
("system", "You are generating a realistic stub response for testing."),
("user", """Generate a realistic stub response for this method:
Method: {method_name}
Arguments: {args}
Keyword Arguments: {kwargs}
Generate appropriate test data as JSON.""")
])
chain = prompt | self.llm
response = chain.invoke({
"method_name": method_name,
"args": str(args),
"kwargs": str(kwargs)
})
try:
result = json.loads(response.content)
except:
result = {"status": "dynamic_stub", "content": response.content}
Explanation: DYNAMIC type: use LLM to generate realistic test data based on method name and arguments.
except Exception as e:
state["insights"] = state.get("insights", [])
state["insights"].append(f"Exception during response generation: {str(e)}")
result = {"error": str(e)}
state["result"] = result
return state
Explanation: Catch exceptions, record as insight, return error dict.
Lines 312-333: Validate Response Node
def _validate_response_node(self, state: StubState) -> StubState:
"""Validate generated response."""
result = state["result"]
stub_type = state["recommended_type"]
# Basic validation
validation_passed = True
if stub_type == StubType.NULL:
validation_passed = result is None
elif stub_type == StubType.EXCEPTION:
validation_passed = isinstance(result, dict) and "error" in result
elif stub_type == StubType.SIMPLE:
validation_passed = result is not None
Explanation: Validate that result matches expected type: – NULL should be None – EXCEPTION should be error dict – SIMPLE should not be None
state["validation_passed"] = validation_passed
if not validation_passed:
state["insights"] = state.get("insights", [])
state["insights"].append(f"Validation failed for {stub_type.value} stub")
return state
Explanation: Store validation result, add insight if failed.
Lines 335-356: Record Call Node
def _record_call_node(self, state: StubState) -> StubState:
"""Record the stub call."""
call = StubCall(
method_name=state["method_name"],
args=state["args"],
kwargs=state["kwargs"],
result=state["result"],
stub_type=state["recommended_type"],
call_number=state["call_number"]
)
self.call_history.append(call)
self.stats["total_calls"] += 1
self.stats["stub_types"][state["recommended_type"].value] += 1
return state
Explanation: Create call record and add to history, update statistics.
Lines 358-384: Public Methods
def register_stub(
self,
method_name: str,
stub_type: StubType = StubType.SIMPLE,
return_value: Any = None,
return_sequence: List[Any] = None,
condition: Optional[Callable] = None,
exception: Optional[Exception] = None,
complexity: StubComplexity = StubComplexity.SIMPLE
):
"""Register a stub configuration."""
stub = StubResponse(
method_name=method_name,
stub_type=stub_type,
return_value=return_value,
return_sequence=return_sequence or [],
condition=condition,
exception=exception,
complexity=complexity
)
self.stubs[method_name] = stub
self.stats["methods_stubbed"] = len(self.stubs)
Explanation: Register stub configuration for a method. Called by users to configure stub behavior.
def call(self, method_name: str, *args, **kwargs) -> Any:
"""Execute stub method call through workflow."""
initial_state: StubState = {
"method_name": method_name,
"args": args,
"kwargs": kwargs,
"stub_config": None,
"recommended_type": None,
"complexity": None,
"analysis": None,
"result": None,
"validation_passed": False,
"insights": [],
"call_number": 0
}
final_state = self.workflow.invoke(initial_state)
return final_state["result"]
Explanation: Main entry point – create initial state, run workflow, return result. This is called when test code invokes the stubbed method.
def get_call_history(self, method_name: str = None) -> List[StubCall]:
"""Get call history."""
if method_name:
return [call for call in self.call_history if call.method_name == method_name]
return self.call_history
Explanation: Retrieve call history, optionally filtered by method name.
Lines 408-445: StubPattern Coordinator
class StubPattern:
"""
Comprehensive Stub Pattern coordinator with LangChain/LangGraph.
"""
def __init__(self, model_name: str = "gpt-4"):
self.managers: Dict[str, IntelligentStubManager] = {}
self.model_name = model_name
Explanation: Coordinator managing multiple stub managers (e.g., one for database, one for API).
def create_manager(self, name: str, target_class: str = "") -> IntelligentStubManager:
"""Create a stub manager."""
manager = IntelligentStubManager(name, model_name=self.model_name)
self.managers[name] = manager
return manager
Explanation: Factory method to create new stub manager.
def call(self, manager_name: str, method_name: str, *args, **kwargs) -> Dict[str, Any]:
"""Execute stub call."""
if manager_name not in self.managers:
return {"error": f"Manager {manager_name} not found"}
manager = self.managers[manager_name]
result = manager.call(method_name, *args, **kwargs)
return {
"manager": manager_name,
"method": method_name,
"result": result,
"call_count": manager.call_counts[method_name]
}
Explanation: Route call to appropriate manager, return result with metadata.
Lines 447-572: Demo Code
The demo demonstrates 5 scenarios:
Scenario 1: Database Stub with Sequence
db_manager.register_stub(
"get_users",
stub_type=StubType.SEQUENCE,
return_sequence=[
[{"id": 1, "name": "Alice"}],
[{"id": 2, "name": "Bob"}],
[{"id": 3, "name": "Charlie"}]
],
complexity=StubComplexity.SIMPLE
)
Explanation: Register sequence stub that returns different users on each call. Useful for testing pagination or iteration.
Scenario 2: API Stub with Conditional Logic
api_manager.register_stub(
"fetch_data",
stub_type=StubType.CONDITIONAL,
return_value={"status": "success", "data": "test_data"},
condition=lambda *args, **kwargs: kwargs.get("authenticated", False),
complexity=StubComplexity.MODERATE
)
Explanation: Register conditional stub that returns success only if authenticated=True in kwargs. Tests authentication logic.
Scenario 3: Dynamic Stub with LLM Generation
result3 = pattern.call("ServiceStub", "process_payment",
amount=100.0, currency="USD", user_id=123)
Explanation: Call unconfigured method – LLM generates realistic payment response based on method name and arguments.
Scenario 4: Exception Stub
error_manager.register_stub(
"dangerous_operation",
stub_type=StubType.EXCEPTION,
exception=Exception("Operation not allowed in test mode"),
complexity=StubComplexity.TRIVIAL
)
Explanation: Register exception stub to test error handling code.
Scenario 5: Simple Fixed Response
config_manager.register_stub(
"get_config",
stub_type=StubType.SIMPLE,
return_value={"timeout": 30, "retries": 3, "debug": True},
complexity=StubComplexity.TRIVIAL
)
Explanation: Register simple stub returning fixed configuration. Most common stub type.
Key Design Patterns
- Strategy Pattern: Different stub types implement different response strategies
- Factory Pattern: StubPattern creates and manages stub managers
- Chain of Responsibility: Workflow nodes process requests sequentially
- Observer Pattern: Call history records all invocations
- State Pattern: Workflow progresses through defined states
Testing Best Practices
- Use SIMPLE stubs for deterministic, repeatable tests
- Use SEQUENCE stubs for testing iteration and pagination
- Use CONDITIONAL stubs for testing branching logic
- Use DYNAMIC stubs when you need varied, realistic test data
- Use EXCEPTION stubs to test error handling paths
- Track call history to verify methods were called correctly
Performance Considerations
- SIMPLE/NULL/EXCEPTION: O(1) – instant response
- SEQUENCE: O(1) – array lookup
- CONDITIONAL: O(1) – single function call
- DYNAMIC: O(LLM) – requires LLM call (~1-3 seconds)
- Memory: O(calls) – stores full call history
Extension Points
- Add new stub types by adding to StubType enum and handling in
_generate_response_node - Enhance validation logic in
_validate_response_node - Add stub recording/replay capabilities
- Implement stub verification assertions
- Add timing constraints for performance testing
Leave a Reply