Skip to content

How to Test Controllers

This guide covers testing pico-fastapi controllers using TestClient, container overrides, and mock services.


Basic Setup

A typical pico-fastapi test bootstraps a container, retrieves the FastAPI app, and uses Starlette's TestClient:

from fastapi import FastAPI
from fastapi.testclient import TestClient
from pico_boot import init


def test_hello_endpoint():
    container = init(modules=["myapp"])
    app = container.get(FastAPI)

    with TestClient(app) as client:
        response = client.get("/api/hello")
        assert response.status_code == 200
        assert response.json() == {"message": "Hello, World!"}

Overriding Services with Mocks

Replace real services with test doubles using container overrides:

from pico_ioc import component


# Production service
@component
class UserService:
    def get_user(self, user_id: int):
        return self.db.query(user_id)  # Hits the database


# Test double
class MockUserService:
    def get_user(self, user_id: int):
        return {"id": user_id, "name": "Test User"}


def test_get_user():
    container = init(
        modules=["myapp"],
        overrides={UserService: MockUserService()},
    )
    app = container.get(FastAPI)

    with TestClient(app) as client:
        response = client.get("/api/users/42")
        assert response.status_code == 200
        assert response.json()["name"] == "Test User"

Shared Fixtures with pytest

Create reusable fixtures in conftest.py:

# conftest.py
import pytest
from fastapi import FastAPI
from fastapi.testclient import TestClient
from pico_boot import init


@pytest.fixture
def container():
    return init(
        modules=["myapp"],
        overrides={
            UserService: MockUserService(),
            EmailService: MockEmailService(),
        },
    )


@pytest.fixture
def app(container):
    return container.get(FastAPI)


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

Then use them in tests:

def test_list_users(client):
    response = client.get("/api/users")
    assert response.status_code == 200


def test_create_user(client):
    response = client.post("/api/users", json={"name": "Alice"})
    assert response.status_code == 201

Testing WebSocket Endpoints

def test_websocket_echo(client):
    with client.websocket_connect("/ws/echo") as ws:
        ws.send_text("ping")
        data = ws.receive_text()
        assert data == "Echo: ping"

Testing Configurers in Isolation

You can test a FastApiConfigurer without bootstrapping the full container:

from fastapi import FastAPI


def test_cors_configurer():
    app = FastAPI()
    configurer = CORSConfigurer()
    configurer.configure_app(app)

    # Verify middleware was added
    middleware_classes = [m.cls.__name__ for m in app.user_middleware]
    assert "CORSMiddleware" in middleware_classes

Disabling Auto-Discovery in Tests

When using pico-boot, plugins are auto-discovered. To disable this in tests:

# conftest.py
import os


def pytest_configure(config):
    os.environ["PICO_BOOT_AUTO_PLUGINS"] = "false"

Testing Tuple Responses

Controllers can return (content, status_code) or (content, status_code, headers) tuples:

def test_not_found_returns_404(client):
    response = client.get("/api/items/999")
    assert response.status_code == 404
    assert response.json() == {"error": "Not found"}

Where the controller method returns:

@get("/items/{item_id}")
async def get_item(self, item_id: int):
    item = self.service.find(item_id)
    if item is None:
        return {"error": "Not found"}, 404
    return item

Common Testing Pitfalls

Problem Cause Fix
NoControllersFoundError Controller module not in modules= list Add the controller module to init(modules=[...])
Stale singleton between tests Container reused across tests Create a fresh container in each test or fixture
WebSocket test hangs No scope="websocket" on controller Add @controller(scope="websocket")
Mock not injected Override key doesn't match the type hint Ensure override key is the exact class used in __init__

Full Example

# test_users.py
import pytest
from fastapi import FastAPI
from fastapi.testclient import TestClient
from pico_boot import init


class MockUserService:
    def __init__(self):
        self.users = {1: {"id": 1, "name": "Alice"}}

    def get_all(self):
        return list(self.users.values())

    def get(self, user_id: int):
        return self.users.get(user_id)

    def create(self, data):
        user = {"id": len(self.users) + 1, **data}
        self.users[user["id"]] = user
        return user


@pytest.fixture
def client():
    container = init(
        modules=["myapp"],
        overrides={UserService: MockUserService()},
    )
    app = container.get(FastAPI)
    with TestClient(app) as c:
        yield c


def test_list_users(client):
    response = client.get("/api/users")
    assert response.status_code == 200
    assert len(response.json()) == 1


def test_create_user(client):
    response = client.post("/api/users", json={"name": "Bob"})
    assert response.status_code == 200
    assert response.json()["name"] == "Bob"


def test_get_user(client):
    response = client.get("/api/users/1")
    assert response.status_code == 200
    assert response.json()["name"] == "Alice"