Testing graphs and nodes

LangGraph · Practical · 11 min

Agents change behaviour silently; tests catch broken routing, state bugs and regressions before users do.

Good LangGraph tests mirror the structure of the graph.

Nodes are functions. Most logic can be tested by calling the node function with a hand-made state dict and checking the returned update. Routing functions too: give them a state, assert the returned node name.

Compiled nodes. graph.nodes["name"].invoke(state) runs a single node as the graph would (skipping any checkpointer).

Whole graphs. Write a create_graph() function returning the builder, and compile it inside each test with a new InMemorySaver(). Then every test gets isolated threads and you can assert on the final state, on get_state(config).next after an interrupt, or on the sequence of updates from stream.

Fake the model. Real model calls are slow, cost money and vary between runs. Inject the model (as a parameter or via runtime context) so tests can pass a fake. langchain_core includes fake chat models such as GenericFakeChatModel, which return messages you script in advance, including AIMessages with tool_calls. That lets you test the agent loop deterministically.

Partial runs. graph.update_state(config, values, as_node="x") lets you set up state as if node x just ran, then continue from there, which is handy for testing late stages without running early ones.

Keep a separate, slower suite (an eval) that runs real models on realistic tasks; LangSmith supports this.

Example

# test_graph.py   (pip install pytest)
from typing import TypedDict
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.graph import StateGraph, START, END


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


def classify(state: State) -> dict:
    return {"label": "spam" if "win money" in state["text"] else "ok"}


def create_graph() -> StateGraph:
    b = StateGraph(State)
    b.add_node(classify)
    b.add_edge(START, "classify")
    b.add_edge("classify", END)
    return b


def test_node_directly():
    assert classify({"text": "win money now", "label": ""}) == {"label": "spam"}


def test_whole_graph():
    graph = create_graph().compile(checkpointer=InMemorySaver())
    out = graph.invoke({"text": "hello"}, {"configurable": {"thread_id": "t1"}})
    assert out["label"] == "ok"

Remember

  • Test node and routing functions as plain functions
  • Compile a fresh builder with a new InMemorySaver per test
  • Inject fake chat models (e.g. GenericFakeChatModel) for deterministic runs
  • update_state(..., as_node=...) sets up mid-graph scenarios

Check yourself

  1. Why compile the graph inside each test with a new InMemorySaver?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: docs.langchain.com/oss/python/langgraph/test