Тестирование логики, завязанной на IndexedDB, требует строгой изоляции состояния между тестами. Dexie.js работает поверх IndexedDB и сохраняет данные в рамках имени базы данных, поэтому любая утечка состояния между тестами приводит к флаки-результатам, особенно при параллельном запуске тестов.
Ключевой принцип тестовой инфраструктуры — каждый тест получает собственный экземпляр базы данных с полностью контролируемым жизненным циклом.
В Node.js отсутствует нативный IndexedDB, поэтому тестовая среда
должна его эмулировать. Наиболее распространённый вариант —
использование fake-indexeddb, который предоставляет
глобальный объект indexedDB, совместимый с Dexie.
Базовая настройка выполняется на уровне тестового раннера:
import 'fake-indexeddb/auto';
import Dexie from 'dexie';
Пакет fake-indexeddb/auto автоматически прокидывает:
indexedDBIDBKeyRangeIDBTransactionDexie начинает работать так же, как в браузере, но все данные остаются в памяти процесса.
Жёсткое правило тестирования 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) управление базой обычно привязывается к хукам.
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();
});
Это полностью исключает:
VersionErrorУдаление всей базы не всегда обязательно. В некоторых сценариях быстрее очищать таблицы:
async function clearTables(db) {
await Promise.all(db.tables.map(table => table.clear()));
}
Особенности:
Однако важно учитывать, что:
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' });
});
При тестировании в Node можно полностью держать данные в памяти.
fake-indexeddb уже обеспечивает это, но важно понимать его
поведение:
Иногда используется явное переопределение:
global.indexedDB = require('fake-indexeddb');
global.IDBKeyRange = require('fake-indexeddb/lib/FDBKeyRange');
Дополнительно можно изолировать не только базу, но и сам Dexie-инстанс:
function createIsolatedDb() {
const DexieClass = Dexie;
const db = new DexieClass(`IsoDB_${crypto.randomUUID()}`);
db.version(1).stores({
items: '++id,value'
});
return db;
}
Такой подход снижает риск:
Даже при новом экземпляре Dexie данные могут сохраняться.
Приводит к накоплению состояния между тестами.
Вызывает случайные VersionError и
InvalidStateError.
Блокируют удаление базы и вызывают зависание тестов.
Обобщённый вариант тестового хелпера:
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);
});
});