LangGraph Tutorial (Python): mocking LLM calls in tests for advanced developers

By Cyprian AaronsUpdated 2026-04-22
langgraphmocking-llm-calls-in-tests-for-advanced-developerspython

This tutorial shows you how to test LangGraph workflows without calling a real model. You’ll replace LLM calls with deterministic mocks so your tests stay fast, cheap, and stable across CI runs.

What You'll Need

  • Python 3.10+
  • langgraph
  • langchain-core
  • pytest
  • pydantic
  • Optional: langchain-openai if you want to compare against a real model later
  • No API key is required for the mocked tests in this tutorial

Step-by-Step

  1. Start with a small graph that calls an LLM through a dependency you can swap out in tests. The key is to keep the model call behind a function boundary, not buried inside the graph node.
from typing import TypedDict
from langgraph.graph import StateGraph, END

class State(TypedDict):
    text: str
    result: str

def call_llm(text: str) -> str:
    raise NotImplementedError("Replace in production or tests")

def analyze(state: State) -> State:
    return {"result": call_llm(state["text"])}

builder = StateGraph(State)
builder.add_node("analyze", analyze)
builder.set_entry_point("analyze")
builder.add_edge("analyze", END)

graph = builder.compile()
  1. Test the graph by monkeypatching the dependency, not by touching LangGraph internals. This keeps your test focused on behavior and avoids brittle assumptions about how nodes are executed.
from app import graph
import app

def test_graph_with_mocked_llm(monkeypatch):
    def fake_llm(text: str) -> str:
        return f"MOCKED::{text.upper()}"

    monkeypatch.setattr(app, "call_llm", fake_llm)

    output = graph.invoke({"text": "hello", "result": ""})
    assert output["result"] == "MOCKED::HELLO"
  1. If your node uses a chat model directly, mock at the method level with unittest.mock. This is useful when you want to keep production code close to LangChain/LangGraph patterns but still avoid network calls in tests.
from unittest.mock import patch
from langchain_core.messages import AIMessage

def test_chat_model_call():
    with patch("app.ChatOpenAI") as mock_cls:
        mock_instance = mock_cls.return_value
        mock_instance.invoke.return_value = AIMessage(content="approved")

        from app import make_decision
        result = make_decision({"text": "check this"})
        assert result["decision"] == "approved"
  1. For more advanced graphs, inject the model as a dependency into the node factory. This makes it easy to pass a fake object in tests and a real model in production without changing graph wiring.
from typing import Callable, TypedDict
from langgraph.graph import StateGraph, END

class State(TypedDict):
    prompt: str
    answer: str

def build_analyzer(model: Callable[[str], str]):
    def analyzer(state: State) -> State:
        return {"answer": model(state["prompt"])}
    return analyzer

def fake_model(prompt: str) -> str:
    return f"fake-response-for:{prompt}"

builder = StateGraph(State)
builder.add_node("analyze", build_analyzer(fake_model))
builder.set_entry_point("analyze")
builder.add_edge("analyze", END)
graph = builder.compile()
  1. If you need deterministic responses across multiple test cases, use pytest parametrization. That lets you cover branching logic in your LangGraph without rewriting setup code for every scenario.
import pytest

@pytest.mark.parametrize(
    "input_text,expected",
    [
        ("fraud alert", "fake-response-for:fraud alert"),
        ("refund request", "fake-response-for:refund request"),
    ],
)
def test_parametrized_graph(input_text, expected):
    output = graph.invoke({"prompt": input_text, "answer": ""})
    assert output["answer"] == expected

Testing It

Run your test suite with pytest -q and confirm there are no network calls or flaky timeouts. If a test fails, check whether you patched the exact symbol used by the node function, not just a class somewhere else in the import tree.

A good sanity check is to temporarily make the fake return an obviously unique string like MOCKED::.... If that string appears in your final state, your mock is wired correctly and your graph is executing through the expected path.

For larger graphs, add one assertion per branch outcome instead of asserting on every intermediate node state. That gives you stable tests while still proving routing logic works.

Next Steps

  • Add stateful mocks that simulate retries, refusals, and malformed outputs.
  • Test conditional edges by mocking different outputs from the same node.
  • Move from plain monkeypatching to dependency-injected model factories for cleaner production code.

Keep learning

By Cyprian Aarons, AI Consultant at Topiax.

Want the complete 8-step roadmap?

Grab the free AI Agent Starter Kit — architecture templates, compliance checklists, and a 7-email deep-dive course.

Get the Starter Kit

Related Guides