Agentic Architecture Pattern: Stub

Written by

in


    """
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

  1. Strategy Pattern: Different stub types implement different response strategies
  2. Factory Pattern: StubPattern creates and manages stub managers
  3. Chain of Responsibility: Workflow nodes process requests sequentially
  4. Observer Pattern: Call history records all invocations
  5. State Pattern: Workflow progresses through defined states

Testing Best Practices

  1. Use SIMPLE stubs for deterministic, repeatable tests
  2. Use SEQUENCE stubs for testing iteration and pagination
  3. Use CONDITIONAL stubs for testing branching logic
  4. Use DYNAMIC stubs when you need varied, realistic test data
  5. Use EXCEPTION stubs to test error handling paths
  6. 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

  1. Add new stub types by adding to StubType enum and handling in _generate_response_node
  2. Enhance validation logic in _validate_response_node
  3. Add stub recording/replay capabilities
  4. Implement stub verification assertions
  5. Add timing constraints for performance testing

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *