Очистка хранилища между тестами

Асинхронные хранилища браузера создают специфическую проблему в тестировании: состояние сохраняется между прогонками тестов, если его явно не сбрасывать. Библиотека localForage, использующая IndexedDB, WebSQL или localStorage в зависимости от окружения, усиливает эту проблему из-за кэширования соединений и отложенных операций записи.

Изоляция тестов в контексте localForage требует дисциплины в управлении жизненным циклом хранилища: инициализация, очистка, возможное пересоздание драйверов и контроль конкурентных операций.

Поведение localForage, влияющее на тестовую изоляцию

localForage работает как абстракция над несколькими механизмами хранения:

  • IndexedDB (основной драйвер в современных браузерах)
  • WebSQL (устаревший, но возможный в некоторых окружениях)
  • localStorage (fallback для ограниченных сред)
  • memoryStorage (часто используется в тестах)

Ключевая особенность — асинхронный API, даже если backend синхронный. Это приводит к тому, что операции записи и очистки не гарантируют мгновенного эффекта без ожидания Promise.

Дополнительно:

  • соединения с IndexedDB могут оставаться открытыми между тестами
  • операции записываются транзакционно и могут завершаться позже, чем ожидается
  • драйвер может сохранять внутренний кэш состояния

Базовый механизм очистки: localForage.clear()

Основной способ сброса состояния:

import localforage from "localforage";

await localforage.clear();

Метод clear() удаляет все ключи текущего экземпляра хранилища. Важно учитывать:

  • операция асинхронная
  • возвращает Promise
  • очищает только текущую конфигурацию instance (driver, name, storeName)

Особенности применения

При использовании нескольких экземпляров:

const storeA = localforage.createInstance({ name: "A" });
const storeB = localforage.createInstance({ name: "B" });

await storeA.clear();

Очистка одного instance не затрагивает другие, если они используют разные namespace.

Полная очистка через dropInstance

В сценариях, где требуется полное удаление базы IndexedDB:

await localforage.dropInstance({
  name: "app-db",
  storeName: "cache"
});

Этот метод:

  • удаляет конкретное хранилище IndexedDB
  • освобождает ресурсы браузера
  • полезен при тестах миграций схемы

Ограничение: поддерживается не всеми драйверами одинаково, особенно memoryStorage.

Подходы к очистке в тестовых фреймворках

Jest: глобальные хуки

Наиболее распространённый подход — использование beforeEach / afterEach.

import localforage from "localforage";

beforeEach(async () => {
  await localforage.clear();
});

afterEach(async () => {
  await localforage.clear();
});

Размещение очистки в обоих хуках снижает риск утечки состояния при прерывании теста.

Vitest: изоляция через setupFiles

import { beforeEach } from "vitest";
import localforage from "localforage";

beforeEach(async () => {
  await localforage.clear();
});

Дополнительно важно учитывать, что Vitest может выполнять тесты параллельно, что увеличивает вероятность гонок.

Mocha: управление через root hooks

beforeEach(async function () {
  await localforage.clear();
});

При асинхронных тестах необходимо использовать done или return Promise.

Проблема гонок между тестами

Асинхронность IndexedDB приводит к ситуации:

  1. тест A вызывает setItem
  2. тест B вызывает clear
  3. запись A завершается позже очистки B

Результат — нестабильное состояние.

Решение: последовательное завершение операций

afterEach(async () => {
  await localforage.clear();
  await new Promise(resolve => setTimeout(resolve, 0));
});

Микрозадержка позволяет завершить pending transactions.

Использование memory driver для тестов

Оптимальная стратегия — замена драйвера:

import localforage from "localforage";
import memoryDriver from "localforage-memoryStorageDriver";

await localforage.defineDriver(memoryDriver);

await localforage.setDriver(memoryDriver._driver);

Преимущества:

  • отсутствие IndexedDB побочных эффектов
  • мгновенная очистка памяти
  • отсутствие блокировок браузера

Очистка в этом случае становится тривиальной:

await localforage.clear();

Пересоздание instance между тестами

Иногда требуется не очистка, а полное пересоздание состояния:

let store;

beforeEach(async () => {
  store = localforage.createInstance({
    name: "test-db"
  });

  await store.clear();
});

Подход полезен при тестировании различных конфигураций driver.

Очистка IndexedDB на уровне браузера

В редких случаях localForage не полностью освобождает IndexedDB соединения. Тогда применяется ручное удаление базы:

const deleteDB = indexedDB.deleteDatabase("test-db");

await new Promise((resolve, reject) => {
  deleteDB.onsucc ess = resolve;
  deleteDB.oner ror = reject;
});

Это гарантирует полное удаление состояния вне зависимости от localForage cache.

Изоляция через уникальные имена баз

Подход, снижающий необходимость очистки:

const store = localforage.createInstance({
  name: `test-db-${Date.now()}`
});

Каждый тест получает уникальную базу, что исключает пересечения.

Недостаток — накопление мусора в IndexedDB при длительных прогонах.

Проблемы параллельного выполнения тестов

При включённом параллелизме:

  • несколько тестов могут использовать один и тот же store
  • очистка одного теста влияет на другой
  • порядок выполнения beforeEach не гарантируется

Решение:

  • уникальные instance per test file
  • отключение parallel execution для тестов storage
  • использование mutex-обёрток над localForage

Обёртка для безопасной очистки

export async function safeClear(store = localforage) {
  try {
    await store.clear();
  } catch (e) {
    await new Promise(resolve => setTimeout(resolve, 50));
    await store.clear();
  }
}

Подход учитывает нестабильность IndexedDB транзакций.

Очистка при тестировании миграций

При проверке схем данных важно гарантировать отсутствие старых ключей:

beforeEach(async () => {
  await localforage.dropInstance({
    name: "app-db"
  });

  await localforage.createInstance({
    name: "app-db"
  });
});

Это позволяет тестировать начальную миграцию без влияния предыдущих структур.

Контроль состояния через инспекцию ключей

Иногда требуется не просто очистка, а проверка:

const keys = await localforage.keys();

for (const key of keys) {
  await localforage.removeItem(key);
}

Такой подход полезен при отладке, когда clear() ведёт себя нестабильно из-за драйвера.

Особенности CI окружений

В CI системах:

  • IndexedDB может эмулироваться поверх файловой системы
  • очистка может выполняться медленнее
  • возможны утечки между worker-процессами

Рекомендуемые меры:

  • обязательный await clear()
  • последовательный запуск тестов для storage слоя
  • использование memoryStorageDriver

Типовые ошибки при очистке

Игнорирование await

beforeEach(() => {
  localforage.clear(); // ошибка
});

Результат — гонки между тестами.

Очистка не того instance

const store = localforage.createInstance({ name: "A" });

await localforage.clear(); // не влияет на store A

Предположение синхронности

IndexedDB всегда асинхронен, даже если API кажется простым.

Стабильная стратегия очистки

Наиболее устойчивый подход включает комбинацию:

  • использование clear() в хуках
  • ожидание завершения микротранзакций
  • изоляция через instance
  • при необходимости dropInstance
  • использование memory driver в тестовой среде

Такая комбинация устраняет большинство проблем, связанных с утечкой состояния между тестами и нестабильностью IndexedDB транзакций.