Dexie.js предоставляет систему хуков (hooks) на уровне таблиц, позволяющую перехватывать операции создания, обновления, удаления и чтения данных. В тестовой среде эти механизмы становятся критически важными для проверки побочных эффектов, целостности данных и предсказуемости поведения слоя доступа к IndexedDB.
Основные типы хуков:
creating — перед добавлением записиreading — при чтении из таблицыupdating — перед обновлением записиdeleting — перед удалением записиХуки выполняются внутри транзакционного контекста Dexie. Это означает, что изменения, внесённые в хуках, участвуют в одной атомарной операции с основным запросом.
Пример регистрации:
db.users.hook('creating', (primKey, obj, transaction) => {
obj.createdAt = Date.now();
});
db.users.hook('updating', (mods, primKey, obj, transaction) => {
mods.updatedAt = Date.now();
});
В тестовой среде это создаёт дополнительный слой сложности: поведение данных зависит не только от вызванного метода, но и от зарегистрированных хуков, которые могут быть определены глобально для базы.
Одной из ключевых проблем является утечка состояния хуков между тестами. Поскольку Dexie хранит хуки на уровне таблицы, их необходимо очищать между сценариями.
Подходы к изоляции:
import Dexie from 'dexie';
function createDb() {
const db = new Dexie('test-db');
db.version(1).stores({
users: '++id, name, age'
});
return db;
}
Каждый тест получает новый экземпляр, исключая накопление хуков.
Dexie позволяет удалять хуки через unregisterHook:
const hook = db.users.hook('creating', () => {});
db.users.hook('creating').unsubscribe(hook);
В тестах это применяется для гарантированного возврата состояния таблицы к исходному виду.
В Node.js среде IndexedDB отсутствует, поэтому тестирование Dexie
требует полифилов. Наиболее распространённый вариант —
fake-indexeddb.
import 'fake-indexeddb/auto';
import Dexie from 'dexie';
Это обеспечивает полноценную эмуляцию IndexedDB, включая транзакции и асинхронное поведение, что критично для проверки хуков.
Особенность: выполнение операций становится полностью синхронным на уровне очереди микротасков, но логически остаётся асинхронным.
Хуки часто используются для модификации данных. Тестирование должно проверять не только вызов операции, но и итоговое состояние объекта.
Пример:
db.users.hook('creating', (primKey, obj) => {
obj.role = obj.role || 'guest';
});
Тест:
await db.users.add({ name: 'Alex' });
const user = await db.users.toArray();
expect(user[0].role).toBe('guest');
Dexie гарантирует последовательность выполнения хуков в порядке регистрации. При тестировании сложных цепочек важно фиксировать порядок вызовов.
const calls = [];
db.users.hook('creating', () => calls.push('first'));
db.users.hook('creating', () => calls.push('second'));
await db.users.add({ name: 'Test' });
expect(calls).toEqual(['first', 'second']);
Хуки выполняются внутри транзакции, что позволяет тестировать атомарность изменений.
db.users.hook('updating', (mods) => {
mods.version = (mods.version || 0) + 1;
});
Проверка:
await db.users.add({ id: 1, version: 1 });
await db.users.update(1, { name: 'Updated' });
const user = await db.users.get(1);
expect(user.version).toBe(2);
Механизм live queries обеспечивает реактивное
обновление результатов запросов при изменении данных в базе. В Dexie это
реализуется через liveQuery, возвращающий
observable-подобный поток.
import { liveQuery } from 'dexie';
const users$ = liveQuery(() => db.users.toArray());
Подписка:
const subscription = users$.subscribe({
next: value => {
console.log(value);
}
});
Любое изменение таблицы автоматически триггерит пересчёт запроса.
Главная сложность — асинхронная реактивность и многократные эмиссии.
Базовая структура теста:
const results = [];
const sub = liveQuery(() => db.users.toArray())
.subscribe({
next: value => results.push(value)
});
await db.users.add({ name: 'A' });
await db.users.add({ name: 'B' });
sub.unsubscribe();
Проблема: эмиссии могут приходить несколько раз на одну операцию из-за транзакционных обновлений.
Для тестов важно учитывать финализацию состояния, а не промежуточные эмиссии.
Подход:
await new Promise(resolve => setTimeout(resolve, 50));
const last = results[results.length - 1];
expect(last.map(u => u.name)).toEqual(['A', 'B']);
Хуки могут изменять данные, которые затем попадают в live query поток. Это создаёт цепочку реактивных обновлений.
db.users.hook('creating', (primKey, obj) => {
obj.createdAt = 1;
});
Live query:
const stream = liveQuery(() => db.users.toArray());
const emitted = [];
stream.subscribe(v => emitted.push(v));
await db.users.add({ name: 'A' });
В этом сценарии важно учитывать, что live query отражает уже модифицированные хуками данные.
Сложные сценарии включают последовательные изменения:
const states = [];
const sub = liveQuery(() => db.users.toArray())
.subscribe(v => states.push(v));
const id = await db.users.add({ name: 'A' });
await db.users.update(id, { name: 'B' });
await db.users.delete(id);
sub.unsubscribe();
Проверка строится на последовательности состояний:
Live queries и hooks опираются на микротаски и транзакции IndexedDB. В тестовой среде это требует явного ожидания стабилизации состояния.
Используются подходы:
setTimeout для завершения очередейLive query подписки сохраняют состояние даже при завершении теста, если не выполнена отписка.
Корректный паттерн:
const sub = liveQuery(() => db.users.toArray()).subscribe(() => {});
sub.unsubscribe();
При отсутствии отписки возможны утечки памяти и ложные срабатывания в последующих тестах.
Dexie группирует изменения в транзакции, и live queries часто обновляются после её завершения.
await db.transaction('rw', db.users, async () => {
await db.users.add({ name: 'A' });
await db.users.add({ name: 'B' });
});
Live query в этом случае должен получить только одно финальное обновление состояния.
Для повышения детерминированности применяются следующие подходы:
В системах, где hooks модифицируют данные, а live queries отражают их состояние, тестирование становится проверкой всей цепочки:
операция → hook → транзакция → IndexedDB → live query → подписка
Каждое звено может влиять на итоговое состояние, поэтому тесты фокусируются на:
При одновременных операциях Dexie сериализует транзакции, но live queries могут получать обновления в разное время.
db.users.bulkAdd([{ name: 'A' }, { name: 'B' }]);
db.users.update(1, { name: 'C' });
Тестирование должно учитывать возможную перестройку состояния несколько раз подряд.
Финальное состояние базы становится основным критерием корректности:
const all = await db.users.toArray();
expect(all.map(x => x.name)).toContain('C');
Промежуточные состояния live query рассматриваются как вспомогательные, но не обязательные для проверки, если не тестируется сама реактивность.