Изоляция тестов и сброс состояния

Работа с IndexedDB через Dexie.js почти всегда связана с состоянием, которое сохраняется между сессиями. В отличие от обычных in-memory структур, база данных живёт независимо от жизненного цикла теста, процесса или даже перезапуска окружения. Это создаёт ключевую проблему тестирования: состояние одного теста может повлиять на другой.

Изоляция тестов в Dexie.js опирается на два базовых принципа: полное управление жизненным циклом базы данных и гарантированное очищение всех ресурсов после выполнения теста.


Особенности состояния IndexedDB в тестовой среде

IndexedDB не является временным хранилищем. Даже при повторном запуске тестов:

  • база данных сохраняется в браузере или эмуляторе;
  • таблицы и индексы остаются неизменными;
  • данные могут накапливаться между прогонами;
  • миграции схемы могут конфликтовать с предыдущими версиями.

В контексте Dexie.js это особенно важно, так как Dexie активно кеширует соединения и управляет версионностью схемы.


Основные стратегии изоляции

Существует несколько устойчивых подходов к обеспечению чистого состояния между тестами:

1. Уникальное имя базы данных

Каждый тест или тестовый набор использует отдельное имя БД:

const db = new Dexie(`TestDB_${Date.now()}_${Math.random()}`);

Такой подход гарантирует отсутствие пересечений, но приводит к накоплению мусора, если база не удаляется.


2. Полное удаление базы данных

Dexie предоставляет прямой механизм удаления:

await db.delete();

или статический вариант:

await Dexie.delete('TestDB');

Этот способ является наиболее надёжным при тестировании, так как полностью удаляет:

  • все таблицы;
  • индексы;
  • данные;
  • метаданные схемы.

Важно учитывать, что удаление базы возможно только при отсутствии открытых соединений.


3. Очистка таблиц без удаления схемы

Если схема стабильна и переиспользуется, можно очищать только данные:

await db.table('users').clear();
await db.table('orders').clear();

или массово через транзакцию:

await db.transaction('rw', db.tables, async () => {
  await Promise.all(db.tables.map(t => t.clear()));
});

Этот подход быстрее удаления базы, но не решает проблему миграций и конфликтов состояния.


Управление жизненным циклом подключения

Dexie активно держит соединение с IndexedDB открытым. Это может мешать:

  • удалению базы;
  • пересозданию схемы;
  • изоляции тестов.

Правильный порядок завершения работы:

db.close();
await db.delete();

Если соединение не закрыто, операция удаления может быть проигнорирована или завершиться неуспешно.


Использование in-memory окружения

В Node.js и некоторых тестовых средах используется эмуляция IndexedDB через fake-indexeddb.

Подключение обычно выглядит так:

import 'fake-indexeddb/auto';
import Dexie from 'dexie';

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

  • отсутствие реального дискового хранения;
  • мгновенный сброс состояния при перезапуске процесса;
  • предсказуемость поведения;
  • удобство CI/CD.

Ограничения:

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

Изоляция в Jest и аналогичных фреймворках

В средах вроде Jest ключевая задача — гарантировать чистый контекст между test или it.

Типовой подход:

let db;

beforeEach(async () => {
  db = new Dexie('TestDB');

  db.version(1).stores({
    users: '++id,name'
  });

  await db.open();
});

afterEach(async () => {
  db.close();
  await db.delete();
});

Критически важно:

  • не переиспользовать глобальные экземпляры Dexie;
  • всегда закрывать соединение;
  • удалять базу после теста, а не только очищать таблицы.

Проблема “залипших” соединений

Одной из наиболее частых причин нестабильных тестов является ситуация, когда:

  • предыдущий тест оставил открытые транзакции;
  • Dexie не успел завершить операции записи;
  • база пытается удалиться в активном состоянии.

Это проявляется в виде:

  • случайных VersionError;
  • невозможности удалить базу;
  • зависаний тестового раннера.

Решение:

  • всегда дожидаться завершения async операций;
  • избегать “fire-and-forget” запросов;
  • использовать await для всех Dexie-операций;
  • закрывать базу до teardown.

Изоляция при изменении схемы

Миграции в Dexie.js добавляют дополнительный слой сложности. Если тесты используют разные версии схемы, необходимо:

  • либо создавать отдельную базу на каждую версию;
  • либо удалять базу перед открытием новой версии;
  • либо строго контролировать порядок версий в тестах.

Пример безопасного паттерна:

await Dexie.delete('TestDB');

const db = new Dexie('TestDB');
db.version(2).stores({
  users: '++id,name,age'
});

await db.open();

Транзакционная изоляция тестовых сценариев

Dexie поддерживает атомарные транзакции, которые могут использоваться для локальной изоляции логики внутри одного теста:

await db.transaction('rw', db.users, async () => {
  await db.users.add({ name: 'A' });
  await db.users.add({ name: 'B' });
});

Однако транзакции не решают проблему глобального состояния между тестами — они изолируют только операции внутри одного выполнения.


Практический шаблон тестовой инфраструктуры

import Dexie from 'dexie';
import 'fake-indexeddb/auto';

export function createTestDB(name = 'TestDB') {
  const db = new Dexie(name);

  db.version(1).stores({
    users: '++id,name'
  });

  return db;
}

export async function setupDB() {
  const db = createTestDB();

  await db.open();

  return db;
}

export async function teardownDB(db) {
  db.close();
  await Dexie.delete(db.name);
}

Такой подход позволяет:

  • централизовать создание базы;
  • гарантировать единый teardown;
  • минимизировать утечки состояния;
  • упростить масштабирование тестов.

Поведение при параллельном запуске тестов

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

  • не использовать одинаковые имена баз;
  • избегать глобальных singleton-инстансов Dexie;
  • учитывать конкуренцию за IndexedDB;
  • использовать изолированные окружения (worker/thread).

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

  • каждая база создаётся заново для теста или группы тестов;
  • соединения всегда закрываются до удаления;
  • очистка выполняется через delete, а не только clear;
  • схемы не пересекаются между тестами;
  • in-memory окружение используется в Node.js через polyfill;
  • любые асинхронные операции полностью завершаются перед teardown.