Testing with Context

Testing applications that use starlette-context requires some special considerations since the context is only available during the request-response cycle. This guide explains how to effectively test your code that relies on the context object.

Testing Endpoints with TestClient

When using Starlette’s TestClient (or FastAPI’s, which is the same), the context is properly set up as part of the request flow:

from starlette.testclient import TestClient
from starlette.applications import Starlette
from starlette.middleware import Middleware
from starlette.responses import JSONResponse
from starlette.routing import Route

from starlette_context import context, plugins
from starlette_context.middleware import ContextMiddleware

async def test_endpoint(request):
    # Access and modify context
    context["test_value"] = "hello"
    return JSONResponse(context.data)

app = Starlette(
    routes=[Route("/test", test_endpoint)],
    middleware=[
        Middleware(
            ContextMiddleware,
            plugins=(plugins.RequestIdPlugin(),)
        )
    ]
)

def test_context_in_endpoint():
    client = TestClient(app)
    response = client.get("/test")

    assert response.status_code == 200

    # Context data is available in the response
    data = response.json()
    assert "X-Request-ID" in data
    assert data["test_value"] == "hello"

Testing Functions That Use Context

For testing functions that use the context directly, you can use the request_cycle_context context manager:

import pytest
from starlette_context import context, request_cycle_context

def function_using_context():
    # Function that uses context
    return context.get("key", "default_value")

def test_function_using_context():
    # Create a context for the test
    with request_cycle_context({"key": "test_value"}):
        result = function_using_context()
        assert result == "test_value"

        # We can also modify the context
        context["another_key"] = "another_value"
        assert context["another_key"] == "another_value"

Mocking the Context Object

In some cases, you might want to mock the entire context object:

from unittest import mock
import pytest
from starlette_context import context
from starlette_context.errors import ContextDoesNotExistError

def test_with_mocked_context():
    # Create a mock context object
    mock_context = mock.MagicMock()
    mock_context.data = {"mocked_key": "mocked_value"}
    mock_context.get.return_value = "mocked_value"

    # Replace the context with our mock
    with mock.patch("your_module.context", mock_context):
        # Test your function that uses context
        from your_module import your_function
        result = your_function()

        # Verify context was used as expected
        mock_context.get.assert_called_with("some_key")

Testing Error Handling

Test how your code handles missing context:

import pytest
from starlette_context import context
from starlette_context.errors import ContextDoesNotExistError

def function_with_context_check():
    if context.exists():
        return context.get("key", "default")
    return "no context"

def test_context_does_not_exist():
    # Outside request-response cycle, no context exists
    result = function_with_context_check()
    assert result == "no context"

    # Accessing context directly should raise an error
    with pytest.raises(ContextDoesNotExistError):
        context["key"] = "value"

Testing Plugins

To test custom plugins, you can use the context middleware with your plugin and make test requests:

import pytest
from starlette.applications import Starlette
from starlette.middleware import Middleware
from starlette.responses import JSONResponse
from starlette.routing import Route
from starlette.testclient import TestClient

from starlette_context import context
from starlette_context.middleware import ContextMiddleware
from my_module import MyCustomPlugin

async def test_endpoint(request):
    return JSONResponse(context.data)

@pytest.fixture
def test_app():
    app = Starlette(
        routes=[Route("/test", test_endpoint)],
        middleware=[
            Middleware(
                ContextMiddleware,
                plugins=(MyCustomPlugin(),)
            )
        ]
    )
    return app

def test_custom_plugin(test_app):
    client = TestClient(test_app)

    # Set custom header that your plugin processes
    response = client.get("/test", headers={"X-Custom-Header": "test-value"})

    assert response.status_code == 200
    data = response.json()

    # Verify your plugin processed the header correctly
    assert "custom_key" in data
    assert data["custom_key"] == "processed-test-value"

Integration Testing

For larger integration tests:

import pytest
from starlette.testclient import TestClient
from your_app import create_app

@pytest.fixture
def client():
    app = create_app()
    with TestClient(app) as client:
        yield client

def test_full_request_flow(client):
    # Make a series of requests that use context
    response1 = client.get("/first-endpoint")
    request_id = response1.json()["X-Request-ID"]

    # Use the request ID in the next request
    response2 = client.get(
        "/second-endpoint",
        headers={"X-Request-ID": request_id}
    )

    # Verify context is maintained
    assert response2.json()["X-Request-ID"] == request_id

Async Testing

For async test functions, you can use pytest-asyncio:

import pytest
import asyncio
from starlette_context import request_cycle_context, context

@pytest.mark.asyncio
async def test_async_function_with_context():
    async def async_function():
        return context.get("key")

    with request_cycle_context({"key": "async_value"}):
        result = await async_function()
        assert result == "async_value"

By using these testing techniques, you can effectively verify that your application is correctly using the context object throughout your request processing flow.