Использование jest-localstorage-mock и аналогов

При тестировании приложений, использующих localForage, часто возникает проблема отсутствия полноценной реализации браузерных механизмов хранения данных в среде выполнения тестов. Большинство тестовых раннеров запускаются в Node.js, где отсутствуют IndexedDB, WebSQL и многие особенности Web Storage API. Даже при использовании JSDOM реализация хранилищ может быть ограниченной или неполной.

Для решения этой задачи применяются специальные библиотеки-моки, которые эмулируют поведение браузерных хранилищ. Они позволяют:

  • запускать тесты без реального браузера;
  • проверять логику сохранения данных;
  • контролировать состояние хранилища между тестами;
  • воспроизводить ошибки чтения и записи;
  • ускорять выполнение тестового набора;
  • изолировать тесты друг от друга.

Одним из наиболее популярных решений является библиотека jest-localstorage-mock, однако для localForage существуют и другие подходы, включая ручное мокирование, использование fake-indexeddb и создание собственных тестовых адаптеров.


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

localForage представляет собой абстракцию над несколькими механизмами хранения:

  • IndexedDB;
  • WebSQL;
  • localStorage.

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

Пример обычного кода:

import localforage from "localforage";

await localforage.setItem("user", {
    id: 1,
    name: "Alex"
});

const user = await localforage.getItem("user");

В браузере такой код работает без дополнительных настроек.

В тестовой среде могут возникнуть ошибки:

No available storage method found.

или

IndexedDB is not defined.

Причина заключается в отсутствии реального механизма хранения данных.


Библиотека jest-localstorage-mock

jest-localstorage-mock предназначена для имитации поведения localStorage в тестах Jest.

После подключения становятся доступны стандартные методы:

localStorage.setItem();
localStorage.getItem();
localStorage.removeItem();
localStorage.clear();

Библиотека хранит данные в памяти и предоставляет дополнительные возможности для проверки вызовов.

Установка:

npm install --save-dev jest-localstorage-mock

или

yarn add -D jest-localstorage-mock

Подключение jest-localstorage-mock

Обычно библиотека подключается через конфигурацию Jest.

Файл:

// jest.config.js

module.exports = {
    setupFiles: [
        "jest-localstorage-mock"
    ]
};

После запуска тестов объект localStorage автоматически становится доступным.


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

Простейший тест может выглядеть следующим образом:

test("сохранение значения", () => {
    localStorage.setItem("token", "123");

    expect(localStorage.getItem("token"))
        .toBe("123");
});

Хранилище работает аналогично браузерному localStorage.


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

Удаление также поддерживается полностью.

test("удаление ключа", () => {
    localStorage.setItem("token", "123");

    localStorage.removeItem("token");

    expect(localStorage.getItem("token"))
        .toBeNull();
});

Очистка хранилища

Метод clear очищает все сохранённые записи.

test("очистка данных", () => {
    localStorage.setItem("a", "1");
    localStorage.setItem("b", "2");

    localStorage.clear();

    expect(localStorage.length).toBe(0);
});

Проверка количества элементов

Мок поддерживает свойство length.

test("количество элементов", () => {
    localStorage.setItem("a", "1");
    localStorage.setItem("b", "2");

    expect(localStorage.length).toBe(2);
});

Проверка вызовов методов

Одним из преимуществ jest-localstorage-mock является интеграция со средствами Jest.

Каждый метод является mock-функцией.

test("проверка вызова setItem", () => {
    localStorage.setItem("token", "abc");

    expect(localStorage.setItem)
        .toHaveBeenCalledWith(
            "token",
            "abc"
        );
});

Проверка количества вызовов:

expect(localStorage.setItem)
    .toHaveBeenCalledTimes(1);

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

Без очистки данные могут сохраняться между тестами.

Стандартная практика:

beforeEach(() => {
    localStorage.clear();
    jest.clearAllMocks();
});

Такой подход обеспечивает полную изоляцию.


Тестирование обёрток над localStorage

Часто создаётся отдельный слой доступа к данным.

Например:

export const saveToken = (token) => {
    localStorage.setItem("token", token);
};

export const getToken = () => {
    return localStorage.getItem("token");
};

Тест:

import {
    saveToken,
    getToken
} from "./storage";

test("сохранение токена", () => {
    saveToken("abc");

    expect(getToken())
        .toBe("abc");
});

Использование jest-localstorage-mock совместно с localForage

Важно понимать, что localForage по умолчанию предпочитает IndexedDB.

Поэтому подключение одного только jest-localstorage-mock может оказаться недостаточным.

Для принудительного использования localStorage можно задать драйвер:

import localforage from "localforage";

beforeAll(async () => {
    await localforage.setDriver(
        localforage.LOCALSTORAGE
    );
});

После этого localForage будет обращаться к мокированному localStorage.


Тестирование localForage через LOCALSTORAGE-драйвер

Пример:

import localforage from "localforage";

beforeAll(async () => {
    await localforage.setDriver(
        localforage.LOCALSTORAGE
    );
});

test("чтение и запись", async () => {
    await localforage.setItem(
        "user",
        "Alex"
    );

    const result =
        await localforage.getItem("user");

    expect(result).toBe("Alex");
});

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


Ограничения jest-localstorage-mock

Несмотря на популярность, библиотека имеет ряд ограничений.

Она не реализует:

  • IndexedDB;
  • WebSQL;
  • транзакции;
  • особенности асинхронной работы IndexedDB;
  • обработку ошибок IndexedDB;
  • поведение драйверов localForage.

Следовательно, тесты могут проходить успешно, хотя приложение будет работать некорректно в реальной IndexedDB-среде.


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

Для более точного тестирования localForage часто применяется библиотека fake-indexeddb.

Она реализует большую часть IndexedDB API в памяти.

Установка:

npm install --save-dev fake-indexeddb

Подключение fake-indexeddb

Файл настройки:

import "fake-indexeddb/auto";

или

// jest.setup.js

require("fake-indexeddb/auto");

После подключения появляется объект:

indexedDB

что позволяет localForage использовать настоящий IndexedDB-драйвер.


Тестирование localForage через IndexedDB

Пример:

import localforage from "localforage";

test("запись пользователя", async () => {
    await localforage.setItem(
        "user",
        {
            id: 1,
            name: "Alex"
        }
    );

    const user =
        await localforage.getItem("user");

    expect(user.name)
        .toBe("Alex");
});

В этом случае тест проходит через IndexedDB-механизм, максимально приближенный к реальному браузеру.


Очистка fake-indexeddb между тестами

Поскольку база данных сохраняется в памяти процесса, необходимо очищать её вручную.

Пример:

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

Либо можно пересоздавать экземпляры localForage.

beforeEach(() => {
    localforage.createInstance({
        name: "test-db"
    });
});

Мокирование localForage через Jest

Иногда требуется полностью заменить localForage фиктивной реализацией.

Пример:

jest.mock("localforage", () => ({
    getItem: jest.fn(),
    setItem: jest.fn(),
    removeItem: jest.fn(),
    clear: jest.fn()
}));

Теперь можно задавать необходимые результаты.

import localforage from "localforage";

localforage.getItem.mockResolvedValue({
    id: 1
});

Частичное мокирование localForage

Полная подмена не всегда удобна.

Часто используется частичное мокирование.

jest.mock("localforage", () => {
    const original =
        jest.requireActual(
            "localforage"
        );

    return {
        ...original,
        setItem: jest.fn()
    };
});

Остальная функциональность сохраняется.


Создание собственного memory storage

Для сложных проектов иногда создаётся отдельный тестовый адаптер.

Простейшая реализация:

const storage = new Map();

export const memoryStorage = {
    async getItem(key) {
        return storage.get(key);
    },

    async setItem(key, value) {
        storage.set(key, value);
        return value;
    },

    async removeItem(key) {
        storage.delete(key);
    },

    async clear() {
        storage.clear();
    }
};

Такой подход обеспечивает полный контроль над поведением тестовой среды.


Тестирование обработки ошибок

Настоящие приложения должны корректно реагировать на ошибки сохранения.

Мокирование позволяет воспроизводить такие сценарии.

localforage.setItem =
    jest.fn().mockRejectedValue(
        new Error("Storage failed")
    );

Проверка:

await expect(
    localforage.setItem(
        "user",
        {}
    )
).rejects.toThrow(
    "Storage failed"
);

Сравнение основных подходов

Подход Скорость Точность Сложность
jest-localstorage-mock высокая низкая низкая
fake-indexeddb высокая высокая средняя
Ручной mock localForage очень высокая низкая низкая
Собственный memory storage высокая средняя высокая

Выбор подхода в зависимости от задачи

jest-localstorage-mock подходит для:

  • проверки бизнес-логики;
  • тестирования сервисов хранения;
  • проверки вызовов localStorage;
  • быстрых unit-тестов.

fake-indexeddb подходит для:

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

Полное мокирование localForage применяется для:

  • изоляции зависимостей;
  • тестирования компонентов верхнего уровня;
  • проверки обработки ошибок;
  • ускорения крупных тестовых наборов.

Собственные memory-хранилища используются при наличии сложных требований к тестовой инфраструктуре, необходимости полного контроля над состоянием данных и создании специализированных тестовых сценариев, которые невозможно реализовать стандартными библиотеками мокирования.