How to test LangGraph graphs with pytest

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:

  1. 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']}"}
  2. 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
  3. 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.

  4. 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.

  5. 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"}
  6. 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.