Stateful graphs can keep routing and checkpoint bugs hidden until a particular input follows the affected path. Automated tests make node transformations and the compiled graph result fail loudly before the behavior reaches an agent workflow.
A function-scoped pytest fixture builds a fresh graph and InMemorySaver for every test, preventing one test's thread state from affecting another. Invoking a compiled node through graph.nodes isolates its transformation and bypasses the graph checkpointer.
The sample uses deterministic string transforms rather than a model call, so it needs no credentials and returns the same assertions on every run. The final test invokes the compiled path with a thread ID and checks the state returned after both nodes.
Steps to test a LangGraph graph with pytest:
- Create the state schema and node functions in graph_app.py.
- graph_app.py
from typing_extensions import TypedDict from langgraph.graph import END, START, StateGraph class GraphState(TypedDict): text: str def normalize_text(state: GraphState) -> GraphState: return {"text": state["text"].strip().lower()} def label_text(state: GraphState) -> GraphState: return {"text": f"processed:{state['text']}"}
- Add the graph builder below the node functions in graph_app.py.
- graph_app.py
def build_graph() -> StateGraph: builder = StateGraph(GraphState) builder.add_node("normalize", normalize_text) builder.add_node("label", label_text) builder.add_edge(START, "normalize") builder.add_edge("normalize", "label") builder.add_edge("label", END) return builder
- Create a function-scoped compiled graph fixture in test_graph_app.py.
- test_graph_app.py
import pytest from langgraph.checkpoint.memory import InMemorySaver from graph_app import build_graph @pytest.fixture def compiled_graph(): return build_graph().compile(checkpointer=InMemorySaver())
A new InMemorySaver keeps checkpoint state isolated for each test that requests the fixture.
- Add a unit test for the normalize node to test_graph_app.py.
- test_graph_app.py
def test_normalize_node(compiled_graph): result = compiled_graph.nodes["normalize"].invoke({"text": " READY "}) assert result == {"text": "ready"}
Calling a node through compiled_graph.nodes tests that node directly without running connected edges or the checkpointer.
- Add an end-to-end state assertion below the node test in test_graph_app.py.
- test_graph_app.py
def test_compiled_graph(compiled_graph): result = compiled_graph.invoke( {"text": " READY "}, config={"configurable": {"thread_id": "graph-test"}}, ) assert result == {"text": "processed:ready"}
- Run pytest from the directory containing both Python files.
$ pytest -q .. [100%] 2 passed in 0.34s
Both dots must reach 100%, and the summary must report two passed tests. Any failed assertion identifies either the isolated node behavior or the compiled graph path that changed.
Mohd Shakir Zakaria is a cloud architect with deep roots in software development and open-source advocacy. Certified in AWS, Red Hat, VMware, ITIL, and Linux, he specializes in designing and managing robust cloud and on-premises infrastructures.