Настройка тестовой базы данных

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

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


Подмена IndexedDB в Node.js окружении

В Node.js отсутствует нативный IndexedDB, поэтому тестовая среда должна его эмулировать. Наиболее распространённый вариант — использование fake-indexeddb, который предоставляет глобальный объект indexedDB, совместимый с Dexie.

Базовая настройка выполняется на уровне тестового раннера:

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

Пакет fake-indexeddb/auto автоматически прокидывает:

  • indexedDB
  • IDBKeyRange
  • IDBTransaction

Dexie начинает работать так же, как в браузере, но все данные остаются в памяти процесса.


Создание фабрики тестовой базы данных

Жёсткое правило тестирования Dexie — никогда не переиспользовать одну и ту же инстанцию между тестами.

Правильный подход — фабрика, создающая новый экземпляр базы данных:

import Dexie from 'dexie';

export function createTestDb(dbName = 'TestDB') {
  const db = new Dexie(dbName);

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

  return db;
}

Ключевые моменты:

  • имя базы можно параметризовать
  • схема фиксируется внутри фабрики
  • миграции ограничиваются тестируемой версией

Полная очистка базы перед каждым тестом

Dexie хранит состояние в рамках имени базы. Поэтому простое создание нового объекта Dexie недостаточно — необходимо удалять старую базу.

Стратегия удаления

import Dexie from 'dexie';

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

Важно:

  • db.close() разрывает активные транзакции
  • Dexie.delete() удаляет физическое хранилище IndexedDB
  • вызов должен быть асинхронным

Жизненный цикл теста

В тестовых фреймворках (Jest, Vitest) управление базой обычно привязывается к хукам.

Пример для Jest

import { createTestDb } from './createTestDb';
import Dexie from 'dexie';

let db;

beforeEach(async () => {
  db = createTestDb('UserTestDB');
  await db.open();
});

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

Поведение:

  • каждая итерация теста получает чистую БД
  • исключаются утечки данных
  • исключаются конфликты версий схемы

Изоляция при параллельном запуске тестов

При параллельном запуске тестов возможны конфликты, если используется одно и то же имя базы.

Решение — динамическая генерация имени:

function uniqueDbName() {
  return `TestDB_${Date.now()}_${Math.random().toString(16).slice(2)}`;
}

Использование:

beforeEach(async () => {
  db = createTestDb(uniqueDbName());
  await db.open();
});

Это полностью исключает:

  • гонки за одну IndexedDB
  • случайное пересечение данных
  • ошибки VersionError

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

Удаление всей базы не всегда обязательно. В некоторых сценариях быстрее очищать таблицы:

async function clearTables(db) {
  await Promise.all(db.tables.map(table => table.clear()));
}

Особенности:

  • сохраняется схема
  • ускоряется выполнение тестов
  • подходит для повторного использования версии БД

Однако важно учитывать, что:

  • open connections остаются активными
  • миграции не перезапускаются

Управление версиями схемы в тестах

Dexie строго контролирует версию базы. Ошибки возникают при попытке открыть БД с меньшей версией схемы после более высокой.

Для тестов рекомендуется:

  • фиксировать версию
  • избегать динамических миграций
  • сбрасывать БД при изменении схемы
db.version(1).stores({
  users: '++id,name'
});

При тестировании миграций каждая версия должна тестироваться отдельно:

const dbV1 = new Dexie('MigrateDB');
dbV1.version(1).stores({ users: '++id,name' });

const dbV2 = new Dexie('MigrateDB');
dbV2.version(2).stores({ users: '++id,name,email' });

Стабильное сидирование тестовых данных

После создания чистой базы часто требуется заполнение фиксированным набором данных.

export async function seedDb(db) {
  await db.users.bulkAdd([
    { name: 'Alice', email: 'a@mail.com' },
    { name: 'Bob', email: 'b@mail.com' }
  ]);

  await db.orders.bulkAdd([
    { userId: 1, total: 100 },
    { userId: 2, total: 200 }
  ]);
}

Использование в тесте:

beforeEach(async () => {
  db = createTestDb('SeedDB');
  await db.open();
  await seedDb(db);
});

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

Одной из частых проблем является утечка открытых соединений Dexie. Она проявляется в виде:

  • невозможности удалить базу
  • ошибки Database is open
  • конфликтов версий

Решение — строгий порядок завершения:

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

Также важно не оставлять незавершённые транзакции:

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

Использование in-memory IndexedDB

При тестировании в Node можно полностью держать данные в памяти. fake-indexeddb уже обеспечивает это, но важно понимать его поведение:

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

Иногда используется явное переопределение:

global.indexedDB = require('fake-indexeddb');
global.IDBKeyRange = require('fake-indexeddb/lib/FDBKeyRange');

Изоляция через отдельные инстансы Dexie

Дополнительно можно изолировать не только базу, но и сам Dexie-инстанс:

function createIsolatedDb() {
  const DexieClass = Dexie;

  const db = new DexieClass(`IsoDB_${crypto.randomUUID()}`);

  db.version(1).stores({
    items: '++id,value'
  });

  return db;
}

Такой подход снижает риск:

  • повторного использования кеша Dexie
  • конфликтов глобальных ссылок
  • неожиданных shared-state эффектов

Типичные ошибки при настройке тестовой базы

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

Даже при новом экземпляре Dexie данные могут сохраняться.

Отсутствие Dexie.delete()

Приводит к накоплению состояния между тестами.

Параллельные тесты без уникальных имен

Вызывает случайные VersionError и InvalidStateError.

Незакрытые транзакции

Блокируют удаление базы и вызывают зависание тестов.


Структурированный helper для тестов

Обобщённый вариант тестового хелпера:

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

export async function withTestDb(callback) {
  const db = new Dexie(`DB_${Date.now()}`);

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

  await db.open();

  try {
    await callback(db);
  } finally {
    db.close();
    await Dexie.delete(db.name);
  }
}

Использование:

test('creates user', async () => {
  await withTestDb(async (db) => {
    await db.users.add({ name: 'Alice' });
    const count = await db.users.count();
    expect(count).toBe(1);
  });
});