Тестирование кода с Dexie в Node.js

Тестирование кода, использующего Dexie.js, в среде Node.js требует эмуляции IndexedDB, поскольку нативная реализация IndexedDB существует только в браузерах. Это создаёт специфический слой инфраструктуры тестирования: вместо реального браузерного хранилища используется in-memory или shim-реализация.

Особенности тестирования IndexedDB-кода вне браузера

IndexedDB обладает рядом характеристик, которые усложняют тестирование:

  • асинхронная модель транзакций
  • отсутствие прямого доступа к файловому хранилищу
  • строгая изоляция между базами данных
  • версия схемы и миграции (upgrades)
  • ограниченная наблюдаемость внутреннего состояния

Dexie.js абстрагирует эти сложности, но не устраняет зависимость от IndexedDB API, поэтому тестовая среда должна воспроизводить его поведение.

Подготовка тестового окружения

Основной подход — подключение реализации IndexedDB для Node.js через fake-indexeddb или аналогичные библиотеки.

Типичный стек:

  • fake-indexeddb
  • dexie
  • jest или vitest

Инициализация полифилла:

import 'fake-indexeddb/auto';
import Dexie fr om 'dexie';

После подключения fake-indexeddb глобальные объекты indexedDB, IDBKeyRange, DOMException становятся доступными, что позволяет Dexie работать без изменений.

Базовая структура тестовой базы

Для тестов обычно создаётся отдельный экземпляр базы данных:

import Dexie from 'dexie';

export function createTestDB() {
  const db = new Dexie('TestDB');

  db.version(1).stores({
    users: '++id, name, age'
  });

  return db;
}

Ключевой принцип — каждый тест должен использовать собственный экземпляр базы или очищать данные между запусками.

Очистка состояния между тестами

Изоляция тестов критична, поскольку IndexedDB сохраняет состояние в рамках процесса.

Стандартный подход:

afterEach(async () => {
  const db = createTestDB();
  await db.delete();
});

Либо более контролируемый вариант:

beforeEach(async () => {
  const db = createTestDB();
  await db.open();
  await db.users.clear();
});

Удаление базы предпочтительнее при сложных схемах с миграциями.

Тестирование операций CRUD

Пример теста добавления данных:

import { createTestDB } from './db';

test('добавление пользователя', async () => {
  const db = createTestDB();

  await db.open();

  const id = await db.users.add({
    name: 'Ivan',
    age: 30
  });

  const user = await db.users.get(id);

  expect(user.name).toBe('Ivan');
  expect(user.age).toBe(30);
});

Dexie обеспечивает транзакционную целостность, поэтому чтение после записи можно тестировать без дополнительных синхронизаций.

Тестирование запросов и индексов

Dexie поддерживает индексированные запросы через where:

test('поиск по индексу', async () => {
  const db = createTestDB();
  await db.open();

  await db.users.bulkAdd([
    { name: 'Anna', age: 25 },
    { name: 'Boris', age: 40 }
  ]);

  const result = await db.users.wh ere('age').above(30).toArray();

  expect(result.length).toBe(1);
  expect(result[0].name).toBe('Boris');
});

Важно учитывать, что поведение индексов в fake-indexeddb может отличаться по производительности, но логическая модель совпадает.

Тестирование транзакций

Транзакции — ключевая часть Dexie. Их поведение особенно важно при сложных операциях:

test('транзакция записи', async () => {
  const db = createTestDB();
  await db.open();

  await db.transaction('rw', db.users, async () => {
    await db.users.add({ name: 'Test1', age: 20 });
    await db.users.add({ name: 'Test2', age: 21 });
  });

  const count = await db.users.count();
  expect(count).toBe(2);
});

Для проверки атомарности можно искусственно вызывать ошибки внутри транзакции:

test('откат транзакции при ошибке', async () => {
  const db = createTestDB();
  await db.open();

  try {
    await db.transaction('rw', db.users, async () => {
      await db.users.add({ name: 'A' });
      throw new Error('fail');
    });
  } catch {}

  const count = await db.users.count();
  expect(count).toBe(0);
});

Тестирование миграций схемы

Версионирование — одна из сильных сторон Dexie.

test('миграция версии базы', async () => {
  const db = new Dexie('MigrateDB');

  db.version(1).stores({
    users: '++id, name'
  });

  db.version(2).stores({
    users: '++id, name, age'
  });

  await db.open();

  await db.users.add({ name: 'Old schema user', age: 50 });

  const user = await db.users.get(1);

  expect(user.age).toBe(50);
});

При тестировании миграций важно использовать чистую базу, иначе IndexedDB может сохранить старую версию схемы между тестами.

Мокирование Dexie-логики

В некоторых случаях требуется изолировать бизнес-логику от реальной базы данных.

Подход — абстракция слоя доступа:

export class UserRepository {
  constructor(db) {
    this.db = db;
  }

  addUser(user) {
    return this.db.users.add(user);
  }

  getAll() {
    return this.db.users.toArray();
  }
}

В тестах можно подменять db на мок:

const mockDb = {
  users: {
    add: jest.fn().mockResolvedValue(1),
    toArray: jest.fn().mockResolvedValue([])
  }
};

Такой подход позволяет тестировать бизнес-логику без IndexedDB.

Использование Vitest и Jest

При использовании jest важно явно подключать polyfill:

import 'fake-indexeddb/auto';

При использовании vitest часто требуется настройка среды:

// vitest.config.js
export default {
  test: {
    environment: 'node'
  }
};

Иногда дополнительно подключается happy-dom, но для Dexie он не обязателен.

Проблемы и ограничения тестирования

  1. Несовпадение поведения IndexedDB реализаций

    • fake-indexeddb не полностью повторяет браузерную реализацию
  2. Отсутствие реальных квот

    • ошибки типа QuotaExceededError могут не воспроизводиться
  3. Различия в транзакционной блокировке

    • параллельные транзакции могут вести себя упрощённо
  4. Сохранение состояния между тестами

    • требуется явное удаление базы

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

Dexie активно используется в асинхронных сценариях:

test('параллельные добавления', async () => {
  const db = createTestDB();
  await db.open();

  await Promise.all([
    db.users.add({ name: 'A' }),
    db.users.add({ name: 'B' }),
    db.users.add({ name: 'C' })
  ]);

  const count = await db.users.count();
  expect(count).toBe(3);
});

Такие тесты помогают выявлять проблемы с гонками, даже в упрощённой среде fake-indexeddb.

Интеграционное тестирование слоя хранения

В более сложной архитектуре Dexie используется как persistence-слой, и тестируется через интеграцию:

  • сервисный слой
  • репозитории
  • бизнес-логика
  • Dexie как реализация storage

Такой подход снижает зависимость тестов от деталей IndexedDB и делает их более устойчивыми к изменениям схемы и библиотек.