Миграции в Dexie.js представляют собой последовательные изменения схемы IndexedDB-базы данных, включающие добавление и удаление таблиц, изменение индексов, трансформацию структуры объектов и перенос данных между версиями. Ошибки в миграциях приводят к потере данных, рассинхронизации схемы и нестабильной работе приложения, поэтому тестирование миграционного слоя становится критически важной частью архитектуры.
Миграции в Dexie.js реализуются через цепочку версий:
db.version(1).stores({
users: '++id,name'
});
db.version(2).stores({
users: '++id,fullName'
}).upgrade(tx => {
return tx.users.toCollection().modify(user => {
user.fullName = user.name;
delete user.name;
});
});
Каждая версия содержит две ключевые части:
stores) — декларативное
описание структурыupgrade) — императивное
преобразование данныхТестирование должно учитывать обе составляющие, поскольку изменение схемы без корректной миграции приводит к частично несовместимым данным.
Основной принцип тестирования миграций — полная изоляция состояния IndexedDB между тестами.
Dexie позволяет использовать отдельные имена баз данных:
const createTestDB = (name) => {
return new Dexie(name);
};
Рекомендуется генерировать уникальные имена баз:
const dbName = `test-db-${Date.now()}-${Math.random()}`;
Это предотвращает утечку состояния между тестами, особенно в средах с параллельным запуском.
Ключевой подход — создание базы с предыдущей версией схемы, заполнение её данными и последующее обновление версии.
const dbV1 = new Dexie('test-db');
dbV1.version(1).stores({
users: '++id,name'
});
await dbV1.open();
await dbV1.users.bulkAdd([
{ name: 'Alice' },
{ name: 'Bob' }
]);
await dbV1.close();
После этого создаётся новая версия с миграцией:
const dbV2 = new Dexie('test-db');
dbV2.version(1).stores({
users: '++id,name'
});
dbV2.version(2).stores({
users: '++id,fullName'
}).upgrade(tx => {
return tx.users.toCollection().modify(u => {
u.fullName = u.name;
delete u.name;
});
});
await dbV2.open();
После выполнения миграции необходимо проверять:
const users = await dbV2.users.toArray();
console.assert(users[0].fullName === 'Alice');
console.assert(users[1].fullName === 'Bob');
Важно проверять не только положительный сценарий, но и отсутствие старых полей:
console.assert(users[0].name === undefined);
Миграции часто включают нетривиальные преобразования:
Пример разбиения поля:
db.version(2).upgrade(tx => {
return tx.users.toCollection().modify(u => {
const [first, last] = u.fullName.split(' ');
u.firstName = first;
u.lastName = last;
delete u.fullName;
});
});
Тест должен проверять граничные случаи:
Миграция должна быть безопасной при повторном применении логики в тестовом контексте. Хотя Dexie применяет upgrade только один раз, тесты могут многократно пересоздавать окружение.
Идея проверки:
Результат должен быть стабилен:
for (let i = 0; i < 3; i++) {
await recreateAndMigrate();
const users = await db.users.toArray();
assertStable(users);
}
В реальных проектах редко бывает одна миграция. Чаще используется цепочка версий:
db.version(1)...
db.version(2)...
db.version(3)...
db.version(4)...
Тестирование должно учитывать переходы:
Особенно важно тестировать пропуск версий, когда пользователь обновляет приложение не последовательно.
IndexedDB может содержать неполные или устаревшие данные. Миграции должны учитывать такие сценарии:
Пример защитной миграции:
db.version(3).upgrade(tx => {
return tx.users.toCollection().modify(u => {
if (!u.fullName && u.name) {
u.fullName = u.name;
}
if (typeof u.fullName !== 'string') {
u.fullName = '';
}
});
});
Тестирование таких сценариев требует ручного создания “грязных” данных.
Для ускорения тестирования миграций часто используется замена реального IndexedDB на in-memory реализации:
Это позволяет:
При больших объёмах данных миграции могут блокировать основной поток. В тестах важно оценивать:
Типовой подход:
console.time('migration');
await db.open();
console.timeEnd('migration');
Для более точного анализа используется разбиение данных на батчи:
tx.users.toCollection().modify(...)
и контроль размера выборки.
Хотя Dexie.js не поддерживает классический rollback миграций, тесты должны учитывать сценарии:
Проверяется поведение:
Иногда upgrade зависит от внешних данных или конфигурации:
Такие зависимости должны быть замокированы:
const getMapping = () => ({ A: 'Alpha', B: 'Beta' });
db.version(2).upgrade(tx => {
return tx.items.toCollection().modify(item => {
item.label = getMapping()[item.code];
});
});
Тесты фиксируют поведение маппинга, чтобы исключить флаки.
Регрессионный подход предполагает:
Сравнение может быть структурным:
или семантическим:
Для сложных систем эффективно использовать генерацию данных:
Это позволяет выявлять неочевидные баги:
Финальный слой проверки связан с целостностью базы:
console.assert(db.tables.length === expectedTables);
Также проверяется выборочная консистентность:
await db.users.where('id').above(0).toArray();
В CI важно учитывать особенности:
Практика:
Покрытие оценивается по нескольким осям:
Полное покрытие миграций означает не только проверку кода, но и проверку всех возможных путей эволюции данных внутри базы.