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

> Learn recommended testing patterns for LangGraph workflows and FastAPI services in Open-Notebook. Master mocking, async testing, and endpoint validation for robust applications.

- Repository: [Luis Novo/open-notebook](https://github.com/lfnovo/open-notebook)
- Tags: testing
- Published: 2026-06-28

---

**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)](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.py`](https://github.com/lfnovo/open-notebook/blob/main/tests/test_graphs.py) → `TestGraphTools`. 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.py`](https://github.com/lfnovo/open-notebook/blob/main/tests/test_graphs.py) → `TestPromptGraph` / `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.py`](https://github.com/lfnovo/open-notebook/blob/main/tests/test_graphs.py) → `TestSaveSourceTitlePreservation`. Patch database objects, feed synthetic state, and assert side effects.

- **FastAPI Endpoints**: Use the `client` fixture from [`conftest.py`](https://github.com/lfnovo/open-notebook/blob/main/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`](https://github.com/lfnovo/open-notebook/blob/main/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.

```python
@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`](https://github.com/lfnovo/open-notebook/blob/main/open_notebook/graphs/source.py), the `save_source` node fetches a `Source` object via `Source.get`, which tests mock to avoid database calls.

```python
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`](https://github.com/lfnovo/open-notebook/blob/main/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.

```python
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`](https://github.com/lfnovo/open-notebook/blob/main/open_notebook/graphs/prompt.py).

```python
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`](https://github.com/lfnovo/open-notebook/blob/main/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.

```python
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.

```python
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

```python
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

```python
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`](https://github.com/lfnovo/open-notebook/blob/main/.github/workflows/test.yml) or locally using:

```bash
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`](https://github.com/lfnovo/open-notebook/blob/main/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`](https://github.com/lfnovo/open-notebook/blob/main/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`](https://github.com/lfnovo/open-notebook/blob/main/tests/test_graphs.py) protects against refactoring that might accidentally remove the decorator from functions in [`open_notebook/graphs/tools.py`](https://github.com/lfnovo/open-notebook/blob/main/open_notebook/graphs/tools.py).

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

Use the `client` fixture from [`tests/conftest.py`](https://github.com/lfnovo/open-notebook/blob/main/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.