Интеграция с Jest и Vitest

Тестирование кода, использующего Dexie.js, требует имитации IndexedDB, поскольку в Node.js среде браузерное API отсутствует. Основная задача тестовой инфраструктуры — воспроизвести поведение IndexedDB максимально близко к реальному браузеру, сохранив детерминированность и скорость выполнения тестов.

В экосистеме тестирования чаще всего применяются два подхода:

  • использование fake-indexeddb для эмуляции IndexedDB в Node.js
  • использование тестового окружения jsdom (в случае Vitest или Jest)
  • комбинация с Dexie тестовыми утилитами

Базовая установка зависимостей:

npm install dexie
npm install -D jest vitest fake-indexeddb

Дополнительно часто подключается dexie-export-import для тестирования миграций и экспорта данных.


Инициализация Dexie в тестовой среде

Dexie при создании базы обращается к глобальному indexedDB. В Node.js его необходимо подменить.

Для Jest:

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

class TestDB extends Dexie {
  constructor() {
    super('TestDB');
    this.version(1).stores({
      users: '++id,name,age'
    });
  }
}

export const db = new TestDB();

Подключение fake-indexeddb/auto автоматически создаёт глобальные реализации:

  • indexedDB
  • IDBKeyRange
  • IDBTransaction

Этого достаточно для большинства сценариев тестирования CRUD-операций Dexie.


Конфигурация Jest для Dexie.js

Jest требует явной настройки среды, если тестируется IndexedDB логика.

Файл jest.config.js:

module.exports = {
  testEnvironment: 'node',
  setupFiles: ['fake-indexeddb/auto']
};

При необходимости изоляции состояния базы добавляется teardown логика:

import { db } from './db';

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

Такой подход предотвращает утечки состояния между тестами и гарантирует чистую базу перед каждым кейсом.


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

Dexie.js предоставляет Promise-based API, что упрощает тестирование через async/await.

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

import { db } from './db';

test('добавление пользователя', async () => {
  const id = await db.users.add({ name: 'Alex', age: 25 });

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

  expect(user.name).toBe('Alex');
  expect(user.age).toBe(25);
});

Обновление записи:

test('обновление пользователя', async () => {
  const id = await db.users.add({ name: 'Anna', age: 30 });

  await db.users.update(id, { age: 31 });

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

  expect(updated.age).toBe(31);
});

Удаление:

test('удаление пользователя', async () => {
  const id = await db.users.add({ name: 'John', age: 40 });

  await db.users.delete(id);

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

  expect(result).toBeUndefined();
});

Очистка базы между тестами

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

Используется один из подходов:

Полное удаление базы

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

Очистка таблиц

afterEach(async () => {
  await db.users.clear();
});

Первый вариант предпочтителен при тестировании миграций и схемы, второй — при тестировании бизнес-логики.


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

Dexie поддерживает атомарные транзакции, которые также подлежат тестированию.

test('транзакция добавления данных', async () => {
  await db.transaction('rw', db.users, async () => {
    await db.users.add({ name: 'Tom', age: 22 });
    await db.users.add({ name: 'Jerry', age: 20 });
  });

  const count = await db.users.count();

  expect(count).toBe(2);
});

Проверка отката:

test('откат транзакции при ошибке', async () => {
  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);
});

Использование Vitest с Dexie.js

Vitest имеет более современную архитектуру и встроенную поддержку ESM, что делает интеграцию с Dexie более прямолинейной.

Установка:

npm install -D vitest fake-indexeddb

Конфигурация vitest.config.js:

import { defineConfig } from 'vitest/config';

export default defineConfig({
  test: {
    environment: 'node',
    setupFiles: ['fake-indexeddb/auto']
  }
});

Особенности выполнения в Vitest

Vitest использует Vite runtime, поэтому важно учитывать:

  • Dexie должен импортироваться как ESM-модуль
  • глобальный indexedDB должен быть инициализирован до импорта базы
  • асинхронные тесты выполняются через native Promise queue

Пример теста:

import { describe, it, expect, beforeEach } from 'vitest';
import { db } from './db';

beforeEach(async () => {
  await db.delete();
  await db.open();
});

describe('Dexie users', () => {
  it('создание записи', async () => {
    const id = await db.users.add({ name: 'Mike' });

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

    expect(user.name).toBe('Mike');
  });
});

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

Dexie поддерживает версионирование базы данных через version().stores(). Тестирование миграций требует последовательного пересоздания базы.

class DB extends Dexie {
  constructor() {
    super('MigrationDB');

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

    this.version(2).stores({
      users: '++id,name,age'
    }).upgrade(tx => {
      return tx.table('users').toCollection().modify(user => {
        user.age = 0;
      });
    });
  }
}

Тест миграции:

test('миграция добавляет поле age', async () => {
  const db1 = new DB();

  await db1.version(1).upgrade();

  await db1.users.add({ name: 'Test' });

  await db1.close();

  const db2 = new DB();

  const user = await db2.users.toArray();

  expect(user[0].age).toBe(0);
});

Использование fake-indexeddb и ограничения

fake-indexeddb покрывает большинство сценариев, но не полностью повторяет поведение браузерного IndexedDB.

Ключевые отличия:

  • упрощённая реализация индексов
  • возможные расхождения в обработке ошибок транзакций
  • отсутствие реальной конкурентности
  • ограниченная эмуляция cursor API

Из-за этого критические участки логики иногда требуют дополнительной проверки в реальном браузере.


Моки Dexie для изоляции бизнес-логики

В некоторых тестах Dexie не требуется как реальная база, достаточно имитации.

Пример мокирования:

jest.mock('./db', () => ({
  db: {
    users: {
      add: jest.fn(),
      get: jest.fn(),
      update: jest.fn()
    }
  }
}));

Такой подход используется для unit-тестирования сервисного слоя, отделённого от хранения данных.


Тестирование реактивных запросов Dexie

Dexie поддерживает live queries через liveQuery и reactive extensions.

import { liveQuery } from 'dexie';

test('реактивный запрос обновляется', async () => {
  const observable = liveQuery(() => db.users.toArray());

  const results = [];

  const subscription = observable.subscribe({
    next: value => results.push(value)
  });

  await db.users.add({ name: 'React' });

  await new Promise(r => setTimeout(r, 50));

  expect(results.length).toBeGreaterThan(0);

  subscription.unsubscribe();
});

В тестах важно учитывать асинхронные задержки реактивных потоков.


Работа с асинхронностью и ожиданиями

Dexie полностью асинхронен, поэтому синхронные assertions приводят к нестабильным тестам.

Рекомендуемые практики:

  • всегда использовать await
  • избегать .then() в тестах
  • использовать waitFor (в случае Vitest + Testing Library)
  • учитывать event loop IndexedDB

Пример ожидания:

await expect(db.users.add({ name: 'Async' }))
  .resolves
  .toBeDefined();

Проверка сложных выборок и индексов

Dexie позволяет использовать индексы, которые также тестируются через queries.

test('поиск по индексу', async () => {
  await db.users.bulkAdd([
    { name: 'Alice', age: 20 },
    { name: 'Bob', age: 30 }
  ]);

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

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

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

test('bulkAdd добавляет несколько записей', async () => {
  await db.users.bulkAdd([
    { name: 'A' },
    { name: 'B' },
    { name: 'C' }
  ]);

  const count = await db.users.count();

  expect(count).toBe(3);
});

Управление состоянием базы в параллельных тестах

При параллельном запуске тестов важно избегать конфликтов имен баз.

Подходы:

  • уникальные имена баз:
const db = new Dexie(`TestDB_${Date.now()}`);
  • последовательный запуск тестов для Dexie модулей
  • изоляция через beforeEach

Поведение ошибок и их тестирование

test('ошибка при невалидной записи', async () => {
  await expect(
    db.users.add(null)
  ).rejects.toThrow();
});

Dexie оборачивает ошибки IndexedDB в собственные исключения, которые также можно проверять через instanceof.

import { Dexie } from 'dexie';

try {
  await db.users.add(null);
} catch (e) {
  expect(e instanceof Dexie.DexieError).toBe(true);
}