Перейти к содержанию

Изоляция и очистка

Два способа начинать каждый тест с одних и тех же данных: откатывать транзакцию вокруг каждого теста или очищать таблицы после него.

Откат транзакции после каждого теста: RollbackIsolation

class RollbackIsolation:
    def __init__(self, context: HareContext | None = None) -> None: ...

    async def __aenter__(self) -> HareContext: ...

    def capture_on_commit(
        self, *, execute: bool = False
    ) -> AbstractAsyncContextManager[list[Callable[[], Any]]]: ...

Самая быстрая изоляция тестов друг от друга: база создаётся один раз на модуль, а каждый тест идёт внутри транзакции, которая откатывается, когда он заканчивается.

import pytest_asyncio
from hare.contrib.test import RollbackIsolation, hare_test_context

@pytest_asyncio.fixture(scope="module")
async def database():
    async with hare_test_context(["library.models"]) as context:
        yield context

@pytest_asyncio.fixture
async def isolated_context(database):
    async with RollbackIsolation(database) as context:
        yield context

Внутри блока каждое подключение контекста (текущего, если context — None) работает в собственной транзакции, а контекст становится текущим; когда блок заканчивается, каждая транзакция откатывается. У подключения к базе без транзакций вместо этого очищаются таблицы его моделей (truncate_all_models(connections=...)). Блок Transactions.atomic(), который открывает тест, — точка сохранения изолирующей транзакции, поэтому тест видит то, что записал, а блок с ошибкой откатывается как обычно. В базе без точек сохранения (ClickHouse с transactions=true) блок вместо этого присоединяется к изолирующей транзакции: записи блока с ошибкой и его колбэки on_commit() остаются до конца изоляции — см. Транзакции ClickHouse.

Изолирующая транзакция никогда не фиксируется: попытка зафиксировать её вручную (await connection.commit()) даёт TransactionManagementError, а колбэки on_commit(), записанные в тесте, сами не выполняются. capture_on_commit() отдаёт их:

isolation = RollbackIsolation(database)
async with isolation:
    async with isolation.capture_on_commit(execute=True) as callbacks:
        await lend_book()            # вызывает Transactions.on_commit(send_due_date_reminder)
    assert len(callbacks) == 1       # send_due_date_reminder выполнился при выходе из блока

Список получает колбэки, записанные в блоке, по порядку, когда блок заканчивается; с execute=True они тут же и выполняются (колбэк async def ожидается), как и колбэки, которые они в свою очередь записывают. Колбэка, записанного в откаченной точке сохранения, среди них нет, как и колбэка, который hare записывает для собственного учёта записи (с execute=True он всё равно выполняется).

Очистка таблиц: truncate_all_models()

async def truncate_all_models(
    context: HareContext | None = None,
    *,
    connections: Collection[DatabaseClient] | None = None,
) -> None: ...

Удаляет все строки из таблицы каждой зарегистрированной модели в context (при None — в текущем), а с connections — только моделей этих подключений, каждой на своём подключении. Таблицы очищает диалект каждого подключения (clear_tables() его редактора схемы); они передаются в порядке внешних ключей — таблица раньше таблиц, на которые она ссылается, — и с Meta.schema, если у диалекта есть схемы:

База данных Поведение
PostgreSQL Одна команда TRUNCATE ... CASCADE по таблицам, в которых есть строки, — сначала он спрашивает, в каких: пустая таблица обходится серверу при очистке так же, как заполненная
SQLite DELETE для каждой таблицы, одним скриптом, при выключенных внешних ключах
ClickHouse TRUNCATE TABLE для каждой таблицы

Автоматически созданные промежуточные таблицы ManyToManyField тоже очищаются. Модель с Meta.managed = False пропускается — её таблицу (или представление) очищать не hare, — как и модель, заменённая другой через свою настройку swappable: у неё нет таблицы. Даёт ConfigurationError, если не загружено ни одно приложение.

topological_sort_models(models) даёт этот порядок для любого списка моделей — модель раньше моделей, на которые она ссылается: в таком порядке можно удалять их строки.