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.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→TestPromptGraph/TestTransformationGraph. Instantiate states and assert that graph objects compile correctly usinghasattr(graph, "invoke"). -
Node Behavior: Test individual async node functions like
save_sourceintests/test_graphs.py→TestSaveSourceTitlePreservation. Patch database objects, feed synthetic state, and assert side effects. -
FastAPI Endpoints: Use the
clientfixture fromconftest.pyto issue real HTTP calls against endpoints likeclient.get("/search?q=foo")and verify status codes and payloads. -
Integration: Test the full stack in
tests/test_models_api.pywhere 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
hasattrchecks to catch import errors early. - Exercise async behavior via
@pytest.mark.asyncioto 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
clientfixture, 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →