Миграции в Dexie.js представляют собой цепочку версий схемы
IndexedDB, каждая из которых может изменять структуру таблиц, индексов и
правил хранения данных. Основная сложность тестирования миграций
заключается в том, что IndexedDB работает в асинхронной среде браузера,
а сам процесс обновления базы данных запускается автоматически через
onupgradeneeded, что делает поведение менее
детерминированным без правильной изоляции.
Ключевой принцип тестирования миграций — воспроизводимость состояния базы на каждой версии схемы и контроль переходов между версиями.
В Node.js и тестовых рантаймах отсутствует нативный IndexedDB, поэтому базовый слой тестирования строится на подмене реализации:
fake-indexeddbdexie с подменённым backendМинимальная настройка среды:
import Dexie fr om "dexie";
import "fake-indexeddb/auto";
Важно, что fake-indexeddb должен подключаться до первого
создания экземпляра базы, иначе Dexie может закешировать нативный
backend (или его отсутствие).
Тест миграции всегда разбивается на три фазы:
Версия 1:
const dbV1 = new Dexie("app");
dbV1.version(1).stores({
users: "id,name"
});
Версия 2:
const dbV2 = new Dexie("app");
dbV2.version(1).stores({
users: "id,name"
});
dbV2.version(2).stores({
users: "id,name,email"
});
Ключевой момент — открытие базы с принудительным переходом версии:
test("migration from v1 to v2 adds email field", async () => {
const db = new Dexie("migration-test");
db.version(1).stores({
users: "id,name"
});
db.version(2).stores({
users: "id,name,email"
});
await db.open();
await db.table("users").add({ id: 1, name: "Alex" });
db.close();
Перезапуск с новой версией:
const upgraded = new Dexie("migration-test");
upgraded.version(1).stores({
users: "id,name"
});
upgraded.version(2).stores({
users: "id,name,email"
});
await upgraded.open();
const users = await upgraded.table("users").toArray();
expect(users[0]).toHaveProperty("name", "Alex");
});
Dexie позволяет явно управлять миграцией через
upgrade:
db.version(2).upgrade(async (tx) => {
const users = tx.table("users");
await users.toCollection().modify(user => {
user.email = "";
});
});
Тестирование таких миграций требует проверки побочных эффектов в данных, а не только схемы.
Сложные миграции часто включают трансформации:
Пример миграции:
db.version(2).upgrade(async (tx) => {
const users = tx.table("users");
for (const user of await users.toArray()) {
user.fullName = user.name.toUpperCase();
delete user.name;
await users.put(user);
}
});
Тест:
const user = await db.table("users").get(1);
expect(user.fullName).toBe("ALEX");
expect(user.name).toBeUndefined();
Миграции должны быть безопасны при повторном запуске логики обновления (в рамках одной версии перехода).
Проверка:
await db.open();
await db.close();
await db.open(); // повторное открытие
const users = await db.table("users").toArray();
expect(users.length).toBe(1);
Каждый тест миграции обязан работать с чистым экземпляром базы:
afterEach(async () => {
await Dexie.delete("migration-test");
});
Критично использовать Dexie.delete, а не только
db.clear(), поскольку schema cache сохраняется в
IndexedDB.
Хотя IndexedDB не поддерживает прямой даунгрейд, можно симулировать сценарии:
Пример:
db.version(3).stores({
users: "id,email"
});
Тест проверяет, что устаревшее поле не используется:
const user = await db.table("users").get(1);
expect(user.name).toBeUndefined();
expect(user.email).toBeDefined();
Миграции выполняются в транзакционном контексте, и тест должен учитывать атомарность:
db.version(2).upgrade(async (tx) => {
const users = tx.table("users");
await users.clear();
await users.add({ id: 1, name: "Migrated" });
});
Проверка:
const users = await db.table("users").toArray();
expect(users).toEqual([
{ id: 1, name: "Migrated" }
]);
Для проверки производительности и стабильности миграций используется генерация данных:
function seedUsers(db, count) {
const batch = [];
for (let i = 0; i < count; i++) {
batch.push({ id: i, name: "User" + i });
}
return db.table("users").bulkAdd(batch);
}
Тест:
await seedUsers(db, 1000);
db.version(2).upgrade(async (tx) => {
await tx.table("users").toCollection().modify(u => {
u.active = true;
});
});
Проверка:
const all = await db.table("users").toArray();
expect(all.every(u => u.active === true)).toBe(true);
Dexie предоставляет информацию о версии базы:
console.log(db.verno);
Полезно в тестах:
expect(db.verno).toBe(2);
fake-indexeddb в Node окруженииupgradeУстойчивый подход строится на следующих принципах:
upgradeОсобое внимание уделяется сценарию, когда данные уже существуют до миграции:
await db.table("users").add({ id: 1, name: "Legacy" });
await db.close();
await db.open(); // триггер миграции
Проверка:
const user = await db.table("users").get(1);
expect(user.name).toBe("Legacy");
Сложные приложения имеют несколько версий подряд:
Тестирование требует проверки не только конечного состояния, но и
корректности промежуточных преобразований через последовательные
Dexie.version() определения.
IndexedDB может прервать апгрейд при ошибке. Тестирование включает сценарии:
upgradedb.version(2).upgrade(() => {
throw new Error("Migration failed");
});
Ожидаемое поведение:
Изменение индексов требует проверки не только данных, но и возможности запросов:
db.version(2).stores({
users: "id,name,email,*tags"
});
Проверка:
const result = await db.table("users")
.wh ere("email")
.equals("test@mail.com")
.toArray();
expect(Array.isArray(result)).toBe(true);
IndexedDB может блокироваться при одновременных соединениях:
db.on("blocked", () => {
console.warn("DB blocked during migration");
});
В тестах важно убедиться, что:
open()