Тестирование хуков и live-запросов

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);

В тестах это применяется для гарантированного возврата состояния таблицы к исходному виду.


Использование fake-indexeddb

В 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-запросы в Dexie.js

Механизм live queries обеспечивает реактивное обновление результатов запросов при изменении данных в базе. В Dexie это реализуется через liveQuery, возвращающий observable-подобный поток.

import { liveQuery } from 'dexie';

const users$ = liveQuery(() => db.users.toArray());

Подписка:

const subscription = users$.subscribe({
  next: value => {
    console.log(value);
  }
});

Любое изменение таблицы автоматически триггерит пересчёт запроса.


Особенности тестирования live queries

Главная сложность — асинхронная реактивность и многократные эмиссии.

Базовая структура теста:

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 queries

Хуки могут изменять данные, которые затем попадают в 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 отражает уже модифицированные хуками данные.


Тестирование реактивных цепочек

Сложные сценарии включают последовательные изменения:

  • создание записи
  • модификация через hook
  • реактивное обновление live query
  • повторное обновление через update
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 между тестами

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 в этом случае должен получить только одно финальное обновление состояния.


Стабильные стратегии тестирования реактивности

Для повышения детерминированности применяются следующие подходы:

  • фиксация времени через fake timers
  • ожидание flush микротасков
  • буферизация эмиссий
  • проверка конечного состояния вместо промежуточного
  • полная очистка базы между тестами

Совмещение hooks и live queries в архитектуре тестов

В системах, где 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 рассматриваются как вспомогательные, но не обязательные для проверки, если не тестируется сама реактивность.