Мокирование idb-keyval в юнит-тестах

idb-keyval — это простая библиотека для работы с IndexedDB в браузере через промисы. Она предоставляет функции get, set, del, clear, keys и update для удобного хранения и извлечения данных. При тестировании приложений, которые используют idb-keyval, часто возникает необходимость изолировать логику приложения от реальной IndexedDB, чтобы юнит-тесты выполнялись быстро, предсказуемо и без побочных эффектов. Для этого применяют мокирование.

Зачем мокировать IndexedDB через idb-keyval

  1. Изоляция тестов: Тесты не зависят от состояния реальной базы данных.
  2. Скорость выполнения: Моки работают мгновенно, промисы не делают лишние операции с диском.
  3. Контроль поведения: Можно имитировать ошибки, возвращать предопределённые значения и проверять вызовы функций.

Подходы к мокированию

1. Мокирование функций с помощью Jest

Если используется Jest, функции idb-keyval можно заменить на заглушки через jest.mock:

import * as idbKeyval from 'idb-keyval';

jest.mock('idb-keyval', () => ({
  get: jest.fn(),
  set: jest.fn(),
  del: jest.fn(),
  clear: jest.fn(),
  keys: jest.fn(),
  update: jest.fn(),
}));

После этого можно задавать конкретное поведение моков:

idbKeyval.get.mockResolvedValue({ username: 'testUser' });
idbKeyval.set.mockResolvedValue(undefined);
  • mockResolvedValue(value) используется для имитации успешного промиса.
  • mockRejectedValue(error) имитирует отказ промиса.

2. Мокирование с использованием собственной обёртки

Для сложных проектов полезно создавать собственную обёртку над idb-keyval. Например:

export const storage = {
  getItem: (key) => idbKeyval.get(key),
  setItem: (key, value) => idbKeyval.set(key, value),
  removeItem: (key) => idbKeyval.del(key),
  clear: () => idbKeyval.clear(),
};

В тестах достаточно мокировать только storage:

jest.mock('./storage', () => ({
  storage: {
    getItem: jest.fn(),
    setItem: jest.fn(),
    removeItem: jest.fn(),
    clear: jest.fn(),
  },
}));

Это позволяет не трогать реальный idb-keyval и централизованно управлять поведением мока.

Примеры типовых тестов

Проверка сохранения данных

import { storage } from './storage';

test('сохраняет объект пользователя', async () => {
  const user = { id: 1, name: 'Alice' };
  storage.setItem.mockResolvedValue(undefined);

  await storage.setItem('user', user);

  expect(storage.setItem).toHaveBeenCalledWith('user', user);
});

Проверка получения данных

test('получает объект пользователя', async () => {
  const user = { id: 1, name: 'Alice' };
  storage.getItem.mockResolvedValue(user);

  const result = await storage.getItem('user');

  expect(result).toEqual(user);
  expect(storage.getItem).toHaveBeenCalledWith('user');
});

Симуляция ошибки

test('обработка ошибки при чтении', async () => {
  const error = new Error('DB error');
  storage.getItem.mockRejectedValue(error);

  await expect(storage.getItem('user')).rejects.toThrow('DB error');
});

Продвинутые техники

  1. Состояние мока через объект Можно хранить данные мока в обычном объекте и имитировать поведение реальной базы:
let mockDB = {};

const mockStorage = {
  getItem: jest.fn((key) => Promise.resolve(mockDB[key])),
  setItem: jest.fn((key, value) => {
    mockDB[key] = value;
    return Promise.resolve();
  }),
  removeItem: jest.fn((key) => {
    delete mockDB[key];
    return Promise.resolve();
  }),
  clear: jest.fn(() => {
    mockDB = {};
    return Promise.resolve();
  }),
};
  1. Восстановление состояния между тестами Использование beforeEach для очистки объекта:
beforeEach(() => {
  mockDB = {};
});
  1. Комбинирование с Spy Можно проверять, какие методы были вызваны и с какими аргументами, не изменяя реализацию промисов.

Мокирование метода update

update немного отличается: принимает ключ и функцию обновления значения:

idbKeyval.update('counter', oldValue => (oldValue || 0) + 1);

Мок можно сделать так:

mockStorage.update = jest.fn((key, updater) => {
  const oldValue = mockDB[key];
  const newValue = updater(oldValue);
  mockDB[key] = newValue;
  return Promise.resolve(newValue);
});

Это позволяет имитировать цепочки обновлений точно так же, как в реальной IndexedDB.

Рекомендации

  • Всегда использовать промисы в моках, чтобы сохранить асинхронное поведение.
  • Для сложных проектов предпочтительно создавать обёртку над idb-keyval, которую затем легко мокировать.
  • Проверять вызовы функций через toHaveBeenCalledWith, чтобы убедиться, что логика приложения формирует правильные ключи и значения.
  • Использовать централизованный объект состояния мока (mockDB) для повторяемости тестов и возможности тестировать сценарии с несколькими изменениями данных.

Эти подходы обеспечивают полный контроль над поведением idb-keyval в тестах, сохраняют чистоту и скорость юнит-тестов и позволяют проверять ошибки, изменения и правильность вызовов методов.