SQLite в тестах FastAPI: почему появляется no such table
Тест создаёт таблицы в SQLite в памяти, а первый запрос FastAPI отвечает no such table. Команда create_all завершилась без ошибки. Проблема может быть в том, что обработчик открыл другое соединение: у обычных отдельных in-memory подключений SQLite разные базы.
База в памяти принадлежит соединению
Строка sqlite:// удобна для теста без файлов, но важно, какие соединения использует engine. TestClient выполняет приложение в другом потоке. Если таблицы созданы в одном подключении, а маршрут читает другое, имя таблицы не найдётся. Настройка check_same_thread=False снимает ограничение потока драйвера, но сама по себе не объединяет разные базы.
Для небольшого последовательного теста используйте StaticPool: он возвращает одно соединение, которое видит все таблицы и записи. Это отдельная настройка тестового окружения. Она не делает один connection подходящим для произвольной параллельной нагрузки; конкурентные тесты требуют выбранной стратегии базы и её ограничений.
Создаём отдельную базу для каждого приложения
Фабрика ниже создаёт engine, таблицу и endpoint. Два её вызова дают две независимые базы. Первый клиент видит свою запись, второй начинает с пустого состояния. Нужны SQLAlchemy 2, FastAPI и HTTPX. Все engine освобождаются после теста.
Сохраните весь блок в файл и запустите Python 3.11+ в отдельном окружении с пакетами fastapi, httpx, sqlalchemy. Для моделей используется Pydantic v2; отдельный сервер для TestClient не нужен.
from typing import Annotated
from fastapi import FastAPI, Depends
from fastapi.testclient import TestClient
from sqlalchemy import create_engine, text
from sqlalchemy.orm import Session
from sqlalchemy.pool import StaticPool
def make_app():
engine = create_engine("sqlite://", poolclass=StaticPool,
connect_args={"check_same_thread": False})
with engine.begin() as connection:
connection.execute(text("CREATE TABLE notes (value TEXT NOT NULL)"))
app = FastAPI()
def get_db():
with Session(engine) as session:
yield session
@app.post("/notes")
def add(value: str, db: Annotated[Session, Depends(get_db)]):
db.execute(text("INSERT INTO notes (value) VALUES (:value)"), {"value": value})
db.commit()
return {"saved": value}
@app.get("/notes")
def read(db: Annotated[Session, Depends(get_db)]):
return db.scalars(text("SELECT value FROM notes ORDER BY value")).all()
return app, engine
app1, engine1 = make_app()
app2, engine2 = make_app()
with TestClient(app1) as first, TestClient(app2) as second:
assert first.post("/notes", params={"value": "A"}).status_code == 200
assert first.get("/notes").json() == ["A"]
assert second.get("/notes").json() == []
engine1.dispose()
engine2.dispose()
print("shared connection within test; independent databases across apps")Как не замаскировать исходную ошибку
Не вызывайте create_all заново в каждом обработчике: это способ спрятать неверную границу ресурсов и лишняя работа на каждом запросе. Создание таблиц должно происходить при подготовке теста. В настоящем приложении изменения схемы обычно проводятся миграциями, а не автоматическим исправлением при чтении.
Проверьте, какой get_db действительно использует маршрут. Если вы подменили другую функцию с таким же именем, приложение всё ещё может обращаться к исходному engine. В dependency_overrides ключом служит объект функции, подключённый к Depends, а не её имя в строке. Оставьте в fixture явное создание engine и установку подмены рядом.
Когда лучше выбрать файл или отдельный сервер
Временный SQLite-файл проще для некоторых параллельных проверок, но имеет собственные ограничения блокировок. А проверка SQL, специфичного для PostgreSQL, должна выполняться на PostgreSQL: SQLite не подтверждает поведение другого диалекта. Выберите самый лёгкий стенд, который действительно проверяет заявленное свойство.
Изоляция между тестами важнее случайно успешного первого запроса. Повторите сценарий с новым приложением и убедитесь, что оно пустое. Затем уберите общий engine из модульного глобального состояния, если тесты ожидают независимые базы. Так порядок запуска тестов перестанет влиять на их результат.
Следующая практика
В открытом уроке fixtures, временной базы и dependency override вы свяжете тестовый клиент с подготовленным хранилищем. Это удобное место, чтобы проверить границы engine и убедиться, что соседние тесты не делят записи. Перейдите к уроку о временной базе в тестах.
Чтобы собрать маршруты, хранение и тесты в одном приложении, откройте бесплатную программу FastAPI. Когда понадобится отдельная PostgreSQL и воспроизводимый запуск окружения, продолжите в Docker и Docker Compose: вы соедините сервис с базой, настроите сеть, том и проверку готовности.
Продолжить на практике
- FastAPI для начинающих: API с базой данных и тестами ↗
Соберёте свой первый полноценный backend на FastAPI: с базой данных, регистрацией, JWT-авторизацией и автотестами. 34 коротких урока и 140 заданий с проверкой — бесплатно.
