Testing Patterns for LangGraph Workflows and FastAPI Services in Open-Notebook

Open-Notebook recommends structuring tests by concern, using pytest-asyncio for async nodes, mocking external dependencies like SurrealDB, validating graph compilation with hasattr checks, and testing FastAPI endpoints through a fixture-based client to ensure robust LangGraph workflows.

The Open-Notebook repository leverages LangGraph to orchestrate complex data-processing pipelines and FastAPI to expose them as HTTP services. Testing these integrated components requires specific patterns that handle async execution, stateful transformations, and external API dependencies. This guide distills the proven testing strategies found in [tests/test_graphs.py](https://github.com/lfnovo/open-notebook/blob/main/tests/test_graphs.py) and the FastAPI test modules, providing concrete examples you can apply to your own LangGraph applications.

Structure Tests by Concern

Organizing your test suite by architectural concern keeps the codebase maintainable and makes failures easier to diagnose. Open-Notebook separates tests into distinct categories based on what they validate.

  • Graph Tools: Test utility functions exposed as LangGraph tools in tests/test_graphs.pyTestGraphTools. Verify return types, formats, and that functions carry proper tool metadata.

  • Graph State Models: Validate typed dictionaries that flow through graphs in tests/test_graphs.pyTestPromptGraph / TestTransformationGraph. Instantiate states and assert that graph objects compile correctly using hasattr(graph, "invoke").

  • Node Behavior: Test individual async node functions like save_source in tests/test_graphs.pyTestSaveSourceTitlePreservation. Patch database objects, feed synthetic state, and assert side effects.

  • FastAPI Endpoints: Use the client fixture from conftest.py to issue real HTTP calls against endpoints like client.get("/search?q=foo") and verify status codes and payloads.

  • Integration: Test the full stack in tests/test_models_api.py where HTTP endpoints internally run LangGraph workflows and assert the final response shape.

Unit Testing LangGraph Nodes

LangGraph nodes are typically async def functions that mutate state and interact with external systems. The Open-Notebook test suite demonstrates how to isolate these nodes for reliable unit testing.

Handle Async Execution with pytest-asyncio

LangGraph nodes run inside an asyncio event loop, so tests must use the pytest-asyncio marker. Wrap each async node test with @pytest.mark.asyncio and use await when calling the node under test.

@pytest.mark.asyncio
async def test_run_transformation_assertion_no_content():
    # Arrange – a state missing `input_text`

    state = {"input_text": None, "transformation": MagicMock(), "source": None}
    config = {"configurable": {"model_id": None}}

    # Act + Assert – the node raises a clear AssertionError

    with pytest.raises(AssertionError, match="No content to transform"):
        await run_transformation(state, config)

Isolate Nodes with Mocked Dependencies

Nodes often touch SurrealDB, external AI providers, or the file system. Tests isolate the node by patching these collaborators using unittest.mock. In open_notebook/graphs/source.py, the save_source node fetches a Source object via Source.get, which tests mock to avoid database calls.

from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from open_notebook.graphs.source import save_source
from open_notebook.domain.notebook import Source

@patch("open_notebook.graphs.source.Source.get")
@pytest.mark.asyncio
async def test_custom_title_preserved(mock_get):
    mock_source = MagicMock(spec=Source)
    mock_source.title = "My Custom Research Title"
    mock_source.save = AsyncMock()
    mock_get.return_value = mock_source

    # Build a realistic content_state payload

    content_state = MagicMock()
    content_state.title = "video.mp4"
    content_state.url = "https://example.com"
    content_state.file_path = None
    content_state.content = "Some content"

    state = {
        "source_id": "source:123",
        "content_state": content_state,
        "embed": False,
        "apply_transformations": [],
    }

    await save_source(state)          # <-- node under test

    assert mock_source.title == "My Custom Research Title"
    mock_source.save.assert_awaited_once()

Cover Edge Cases and State Mutations

The save_source node implements sophisticated title logic to preserve user-set titles, replace placeholders, and fill missing values. Tests exhaustively cover each branch by reusing the same mock setup while swapping mock_source.title and content_state.title values.

Scenario Expectation
User-provided title ("My Custom Research Title") Unchanged
Placeholder title ("Processing...") Replaced by extracted title
None title Replaced by extracted title
Empty string ("") Replaced by extracted title

This pattern ensures full branch coverage without requiring a real database connection.

Validating Graph Structure and Tools

Beyond testing individual nodes, verify that the LangGraph composition itself is sound and that tools are correctly registered.

Verify Tool Metadata Registration

LangGraph tools in open_notebook/graphs/tools.py are ordinary functions wrapped by a @tool decorator. Tests confirm the decorator added the expected attributes to prevent accidental refactoring that drops the decorator.

from open_notebook.graphs.tools import get_current_timestamp

def test_get_current_timestamp_is_tool():
    assert hasattr(get_current_timestamp, "name")
    assert hasattr(get_current_timestamp, "description")

Smoke Test Graph Compilation

Before invoking a graph, ensure the object is instantiated and exposes the required methods (invoke, ainvoke). This guards against broken imports or missing nodes in open_notebook/graphs/prompt.py.

def test_prompt_graph_compilation():
    assert graph is not None
    assert hasattr(graph, "invoke")
    assert hasattr(graph, "ainvoke")

This quick validation runs in microseconds but catches many import-time errors early.

Testing FastAPI Integration

Open-Notebook exposes LangGraph workflows through FastAPI endpoints, requiring tests that validate the HTTP layer and the internal graph execution.

Use the Client Fixture for HTTP Tests

The repository provides a client fixture in tests/conftest.py that launches the FastAPI app in a test server. Tests issue HTTP requests just like real users, but the server runs in-process for speed and determinism.

def test_search_successful(client):
    response = client.get("/search?q=python")
    assert response.status_code == 200
    data = response.json()
    assert "results" in data
    assert isinstance(data["results"], list)

End-to-End Integration Testing

For endpoints that trigger LangGraph workflows, test the full request-response cycle. The test validates that the HTTP layer correctly serializes input, the graph processes it, and the response matches the expected schema.

def test_chat_endpoint(client):
    payload = {"messages": [{"role": "user", "content": "Hello"}]}
    resp = client.post("/chat", json=payload)
    assert resp.status_code == 200
    data = resp.json()
    # The endpoint ultimately invokes the chat graph; we just check the shape

    assert "reply" in data
    assert isinstance(data["reply"], str)

Complete Code Examples

The following snippets demonstrate the complete patterns found in the Open-Notebook test suite.

Testing a LangGraph Tool

from open_notebook.graphs.tools import get_current_timestamp

def test_timestamp_format_and_validity():
    ts = get_current_timestamp.func()          # Call the raw function

    # Expected format: YYYYMMDDHHmmss (14 digits)

    assert isinstance(ts, str) and ts.isdigit() and len(ts) == 14

    # Verify it parses back to a datetime object

    from datetime import datetime
    dt = datetime.strptime(ts, "%Y%m%d%H%M%S")
    assert isinstance(dt, datetime)

Testing an Async Node with Patched DB Model

from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from open_notebook.graphs.source import save_source
from open_notebook.domain.notebook import Source

@pytest.mark.asyncio
@patch("open_notebook.graphs.source.Source.get")
async def test_placeholder_title_replaced(mock_get):
    # Mock the persisted Source instance

    source = MagicMock(spec=Source)
    source.title = "Processing..."
    source.save = AsyncMock()
    mock_get.return_value = source

    # Simulate extraction output

    content_state = MagicMock()
    content_state.title = "Extracted Article Title"
    content_state.url = "https://example.com"
    content_state.file_path = None
    content_state.content = "Lorem ipsum"

    state = {
        "source_id": "source:123",
        "content_state": content_state,
        "embed": False,
        "apply_transformations": [],
    }

    await save_source(state)

    assert source.title == "Extracted Article Title"
    source.save.assert_awaited_once()

Running the Full Test Suite

Execute all tests via the CI workflow defined in .github/workflows/test.yml or locally using:

uv run pytest

This command runs both sync and async tests, aggregates coverage, and fails fast on any assertion or exception.

Summary

  • Unit-test each node with isolated mocks for external dependencies like SurrealDB and AI providers.
  • Validate state shapes and tool metadata to ensure LangGraph components are correctly registered.
  • Smoke-test graph compilation using hasattr checks to catch import errors early.
  • Exercise async behavior via @pytest.mark.asyncio to properly handle LangGraph's async node functions.
  • Cover edge cases such as missing titles, empty content, and placeholder replacements for robust state management.
  • Test FastAPI endpoints through the client fixture, ensuring the full stack from HTTP to service to LangGraph works correctly.

Frequently Asked Questions

How do you test async LangGraph nodes without a real database?

Use @pytest.mark.asyncio to handle the event loop and patch database access methods like Source.get with MagicMock and AsyncMock. This isolates the node logic in open_notebook/graphs/source.py from external dependencies while allowing you to assert that save() was called with the correct data.

Why is it important to test graph compilation separately?

Testing that the graph object has invoke and ainvoke attributes—assert hasattr(graph, "invoke")—serves as a smoke test that catches broken imports, missing nodes, or wiring errors in open_notebook/graphs/prompt.py before you attempt to run expensive integration tests.

What is the best way to verify LangGraph tool registration?

Check that the @tool decorator successfully attached metadata by asserting hasattr(function, "name") and hasattr(function, "description"). This simple validation in tests/test_graphs.py protects against refactoring that might accidentally remove the decorator from functions in open_notebook/graphs/tools.py.

How do you test FastAPI endpoints that internally trigger LangGraph workflows?

Use the client fixture from tests/conftest.py to make HTTP requests against the running test server. Assert on the response status code and payload structure, which indirectly validates that the endpoint successfully invoked the LangGraph workflow and handled the state transformations correctly.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →