Как тестировать БД, если она не поддерживает транзакции?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Стратегии тестирования БД без поддержки транзакций
Когда база данных не поддерживает транзакции (например, SQLite в режиме по умолчанию, или NoSQL базы вроде MongoDB), стандартный подход с rollback в конце тестов становится невозможным. Однако существует несколько эффективных стратегий изоляции тестов.
1. Использование отдельной БД для каждого теста
Этот подход гарантирует полную изоляцию тестов. Каждый тест работает с чистой копией БД:
import tempfile
import shutil
from pathlib import Path
import pytest
class DatabaseManager:
def __init__(self):
self.test_dirs = []
def create_test_db(self, db_path="/tmp/test_db"):
"""Создаёт новую директорию БД для теста"""
test_dir = tempfile.mkdtemp(prefix="test_db_")
self.test_dirs.append(test_dir)
return test_dir
def cleanup(self):
"""Удаляет все временные БД"""
for test_dir in self.test_dirs:
if Path(test_dir).exists():
shutil.rmtree(test_dir)
@pytest.fixture
def db_manager():
manager = DatabaseManager()
yield manager
manager.cleanup()
@pytest.fixture
def test_db(db_manager):
return db_manager.create_test_db()
def test_insert_data(test_db):
# Каждый тест получает чистую БД
db = MyDatabase(test_db)
db.insert({"id": 1, "name": "Alice"})
assert db.find_by_id(1)["name"] == "Alice"
2. Очистка данных между тестами (Truncate)
Для реляционных БД используем DELETE/TRUNCATE для очистки таблиц:
import pytest
from sqlalchemy import text
@pytest.fixture
def db_session(database_url):
"""Создаёт сессию и очищает таблицы после теста"""
from sqlalchemy import create_engine
from sqlalchemy.orm import Session
engine = create_engine(database_url)
session = Session(engine)
yield session
# Очистка всех таблиц
session.execute(text("TRUNCATE TABLE users CASCADE"))
session.execute(text("TRUNCATE TABLE orders CASCADE"))
session.execute(text("TRUNCATE TABLE products CASCADE"))
session.commit()
session.close()
def test_user_creation(db_session):
user = User(id=1, name="Bob")
db_session.add(user)
db_session.commit()
found_user = db_session.query(User).filter_by(id=1).first()
assert found_user.name == "Bob"
3. Mock/In-Memory БД
Для быстрых unit-тестов используем in-memory версии БД или полные моки:
import pytest
from unittest.mock import MagicMock, patch
class MockDatabase:
def __init__(self):
self.data = {}
self.counter = 0
def insert(self, table, record):
self.counter += 1
record["id"] = self.counter
if table not in self.data:
self.data[table] = []
self.data[table].append(record)
return record
def find_by_id(self, table, id):
return next(
(r for r in self.data.get(table, []) if r["id"] == id),
None
)
@pytest.fixture
def mock_db():
return MockDatabase()
def test_user_insert(mock_db):
user = mock_db.insert("users", {"name": "Charlie"})
assert user["id"] == 1
assert mock_db.find_by_id("users", 1)["name"] == "Charlie"
4. Снимки состояния (Snapshots)
Для NoSQL БД (MongoDB, DynamoDB) используем снимки и сравнение состояний:
import json
import pytest
@pytest.fixture
def mongo_db():
"""Подключается к тестовой MongoDB"""
from pymongo import MongoClient
client = MongoClient("mongodb://localhost:27017")
db = client["test_db"]
yield db
# Очистка всех коллекций
for collection_name in db.list_collection_names():
db[collection_name].delete_many({})
client.close()
def test_mongodb_insert(mongo_db):
collection = mongo_db["users"]
result = collection.insert_one({"name": "Diana", "age": 25})
# Проверяем состояние
found = collection.find_one({"_id": result.inserted_id})
assert found["name"] == "Diana"
5. Docker контейнеры для каждого теста
Для интеграционных тестов поднимаем отдельный контейнер БД:
import pytest
from testcontainers.mongodb import MongoDbContainer
@pytest.fixture(scope="function")
def mongo_container():
"""Поднимает новый MongoDB контейнер для теста"""
with MongoDbContainer() as container:
yield container.get_connection_string()
def test_with_docker_mongo(mongo_container):
from pymongo import MongoClient
client = MongoClient(mongo_container)
db = client["test"]
# Тест в изолированном контейнере
db.users.insert_one({"name": "Eve"})
assert db.users.count_documents({}) == 1
Сравнение подходов
| Подход | Скорость | Надёжность | Сложность | Лучше для |
|---|---|---|---|---|
| Отдельная БД | Средняя | Высокая | Средняя | Интеграционные тесты |
| Truncate | Высокая | Высокая | Низкая | Реляционные БД |
| Mock | Очень высокая | Средняя | Низкая | Unit-тесты |
| Docker | Средняя | Очень высокая | Высокая | Полная изоляция |
| Snapshots | Высокая | Высокая | Средняя | NoSQL БД |
Рекомендуемая стратегия
- Unit-тесты: используй Mock для максимальной скорости
- Integration-тесты: Truncate + фикстуры для очистки
- E2E-тесты: Docker контейнеры для полной изоляции
- CI/CD: комбинируй все подходы — быстрые моки + медленные интеграционные тесты
Главное правило: каждый тест должен быть независим и не влиять на другие тесты. Выбирай подход в зависимости от типа тестов и требований к скорости.