При тестировании приложений, использующих localForage, часто возникает проблема отсутствия полноценной реализации браузерных механизмов хранения данных в среде выполнения тестов. Большинство тестовых раннеров запускаются в Node.js, где отсутствуют IndexedDB, WebSQL и многие особенности Web Storage API. Даже при использовании JSDOM реализация хранилищ может быть ограниченной или неполной.
Для решения этой задачи применяются специальные библиотеки-моки, которые эмулируют поведение браузерных хранилищ. Они позволяют:
Одним из наиболее популярных решений является библиотека jest-localstorage-mock, однако для localForage существуют и другие подходы, включая ручное мокирование, использование fake-indexeddb и создание собственных тестовых адаптеров.
localForage представляет собой абстракцию над несколькими механизмами хранения:
При запуске приложения библиотека автоматически выбирает наиболее подходящий драйвер.
Пример обычного кода:
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 предназначена для имитации поведения localStorage в тестах Jest.
После подключения становятся доступны стандартные методы:
localStorage.setItem();
localStorage.getItem();
localStorage.removeItem();
localStorage.clear();
Библиотека хранит данные в памяти и предоставляет дополнительные возможности для проверки вызовов.
Установка:
npm install --save-dev jest-localstorage-mock
или
yarn add -D 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();
});
Такой подход обеспечивает полную изоляцию.
Часто создаётся отдельный слой доступа к данным.
Например:
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");
});
Важно понимать, что localForage по умолчанию предпочитает IndexedDB.
Поэтому подключение одного только jest-localstorage-mock может оказаться недостаточным.
Для принудительного использования localStorage можно задать драйвер:
import localforage from "localforage";
beforeAll(async () => {
await localforage.setDriver(
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");
});
Такой вариант подходит для большинства сценариев бизнес-логики.
Несмотря на популярность, библиотека имеет ряд ограничений.
Она не реализует:
Следовательно, тесты могут проходить успешно, хотя приложение будет работать некорректно в реальной IndexedDB-среде.
Для более точного тестирования localForage часто применяется библиотека fake-indexeddb.
Она реализует большую часть IndexedDB API в памяти.
Установка:
npm install --save-dev fake-indexeddb
Файл настройки:
import "fake-indexeddb/auto";
или
// jest.setup.js
require("fake-indexeddb/auto");
После подключения появляется объект:
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-механизм, максимально приближенный к реальному браузеру.
Поскольку база данных сохраняется в памяти процесса, необходимо очищать её вручную.
Пример:
afterEach(async () => {
await localforage.clear();
});
Либо можно пересоздавать экземпляры localForage.
beforeEach(() => {
localforage.createInstance({
name: "test-db"
});
});
Иногда требуется полностью заменить 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
});
Полная подмена не всегда удобна.
Часто используется частичное мокирование.
jest.mock("localforage", () => {
const original =
jest.requireActual(
"localforage"
);
return {
...original,
setItem: jest.fn()
};
});
Остальная функциональность сохраняется.
Для сложных проектов иногда создаётся отдельный тестовый адаптер.
Простейшая реализация:
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 подходит для:
fake-indexeddb подходит для:
Полное мокирование localForage применяется для:
Собственные memory-хранилища используются при наличии сложных требований к тестовой инфраструктуре, необходимости полного контроля над состоянием данных и создании специализированных тестовых сценариев, которые невозможно реализовать стандартными библиотеками мокирования.