Testing with TestClient & pytest
supertest / Jest → TestClient + pytest
Section titled “supertest / Jest → TestClient + pytest”In Node you use supertest to make HTTP requests against your Express or NestJS app in-process, and Jest as the test runner. In FastAPI you use TestClient (from Starlette, bundled with FastAPI) for in-process HTTP requests, and pytest as the test runner.
// Jest + supertest — app.test.tsimport request from 'supertest';import { app } from '../src/app';
describe('GET /items/:id', () => { it('returns an item', async () => { const res = await request(app).get('/items/1'); expect(res.status).toBe(200); expect(res.body.id).toBe(1); });
it('returns 404 for unknown id', async () => { const res = await request(app).get('/items/9999'); expect(res.status).toBe(404); });});# pytest + TestClient — test_items.pyfrom fastapi.testclient import TestClientfrom app.main import app
client = TestClient(app)
def test_get_item(): response = client.get("/items/1") assert response.status_code == 200 assert response.json()["id"] == 1
def test_get_item_not_found(): response = client.get("/items/9999") assert response.status_code == 404Testing POST with a request body
Section titled “Testing POST with a request body”// supertest POSTit('creates an item', async () => { const res = await request(app) .post('/items') .send({ name: 'Widget', price: 9.99 }) .set('Content-Type', 'application/json');
expect(res.status).toBe(201); expect(res.body.name).toBe('Widget');});
it('rejects invalid input', async () => { const res = await request(app) .post('/items') .send({ name: '', price: -1 }); expect(res.status).toBe(422);});# TestClient POSTdef test_create_item(): response = client.post( "/items", json={"name": "Widget", "price": 9.99}, ) assert response.status_code == 201 assert response.json()["name"] == "Widget"
def test_create_item_invalid(): response = client.post( "/items", json={"name": "", "price": -1}, ) assert response.status_code == 422Overriding dependencies in tests
Section titled “Overriding dependencies in tests”The key testing pattern in FastAPI is app.dependency_overrides — replacing real dependencies (like a database session) with test doubles. This is the equivalent of Jest’s jest.mock() or NestJS’s overrideProvider().
// NestJS — override a provider in testsconst moduleRef = await Test.createTestingModule({ providers: [ItemsService],}).overrideProvider(DatabaseService).useValue({ findById: jest.fn().mockResolvedValue({ id: 1 }) }).compile();# FastAPI — dependency_overridesfrom fastapi.testclient import TestClientfrom app.main import appfrom app.database import get_db
# A fake DB session for testsdef override_get_db(): yield FakeDBSession()
app.dependency_overrides[get_db] = override_get_db
client = TestClient(app)
def test_get_item_with_fake_db(): response = client.get("/items/1") assert response.status_code == 200
# Restore after tests (or use a pytest fixture)app.dependency_overrides.clear()pytest fixtures for setup/teardown
Section titled “pytest fixtures for setup/teardown”pytest’s @pytest.fixture is the equivalent of Jest’s beforeEach / afterEach. A conftest.py file shares fixtures across test files — like a Jest global setup file.
// Jest beforeEach / afterEachlet client: TestClient;
beforeAll(async () => { const app = await NestFactory.create(AppModule); await app.init(); client = app.getHttpServer();});
afterAll(async () => { await app.close(); });# pytest — conftest.py fixturesimport pytestfrom fastapi.testclient import TestClientfrom app.main import appfrom app.database import Base, engine, get_dbfrom sqlalchemy import create_enginefrom sqlalchemy.orm import sessionmaker
TEST_DB_URL = "sqlite:///./test.db"test_engine = create_engine(TEST_DB_URL, connect_args={"check_same_thread": False})TestSession = sessionmaker(bind=test_engine)
@pytest.fixture(scope="module")def client(): Base.metadata.create_all(bind=test_engine) def override_db(): db = TestSession() try: yield db finally: db.close() app.dependency_overrides[get_db] = override_db with TestClient(app) as c: yield c Base.metadata.drop_all(bind=test_engine)Testing async routes: httpx AsyncClient
Section titled “Testing async routes: httpx AsyncClient”TestClient runs your async handlers on its own event loop, which is fine for most tests. But when the test itself needs to be async — because it awaits a DB fixture or drives a background task — drive the app with httpx’s AsyncClient over ASGITransport. This is the current idiom for async FastAPI tests and needs no running server.
// Node — supertest is sync-ish; for async setup you await it yourselfimport request from 'supertest';
test('GET /items', async () => { await seedDatabase(); const res = await request(app).get('/items'); expect(res.status).toBe(200);});# pytest + httpx AsyncClient — test_items_async.pyimport pytestfrom httpx import ASGITransport, AsyncClientfrom app.main import app
@pytest.mark.anyioasync def test_list_items(seeded_db): transport = ASGITransport(app=app) async with AsyncClient(transport=transport, base_url="http://test") as client: response = await client.get("/items")
assert response.status_code == 200 assert response.json() == []
# anyio needs a backend; pin it once in conftest.py:@pytest.fixturedef anyio_backend(): return "asyncio"pytest.mark.anyio comes with the anyio plugin that ships with Starlette’s test dependencies; the pytest-asyncio plugin with @pytest.mark.asyncio is the common alternative. Either way you must tell pytest that the test function is a coroutine — unlike Jest, plain pytest will otherwise collect the coroutine and never await it.
Run this locally — needs FastAPI + httpx + pytest. Install:
uv add --dev pytest pytest-cov httpx anyio(orpip installthe same). Playground is skipped: TestClient requires a full FastAPI app and HTTP lifecycle.