При разработке приложений, использующих localForage, значительная часть логики зависит от операций чтения и записи данных. В реальном окружении библиотека работает поверх IndexedDB, WebSQL или LocalStorage, однако во время выполнения юнит-тестов использование настоящих браузерных хранилищ часто приводит к проблемам:
По этой причине в юнит-тестировании широко применяется мокирование localForage — подмена реальной реализации специальными объектами, имитирующими поведение библиотеки.
LocalForage предоставляет набор методов для работы с данными:
localforage.setItem()
localforage.getItem()
localforage.removeItem()
localforage.clear()
localforage.keys()
localforage.length()
localforage.iterate()
localforage.dropInstance()
В большинстве тестов достаточно мокировать только используемые методы.
Например, если приложение лишь сохраняет и получает настройки пользователя, могут понадобиться только:
localforage.setItem
localforage.getItem
Полное покрытие API требуется значительно реже.
Наиболее понятный вариант — создание объекта, повторяющего интерфейс localForage.
Исходный код приложения:
import localforage from "localforage";
export async function saveTheme(theme) {
await localforage.setItem("theme", theme);
}
Мок для тестов:
const localforageMock = {
setItem: jest.fn(),
getItem: jest.fn()
};
Подключение:
jest.mock("localforage", () => localforageMock);
Тест:
import localforage from "localforage";
import { saveTheme } from "./theme";
test("сохраняет тему", async () => {
await saveTheme("dark");
expect(localforage.setItem)
.toHaveBeenCalledWith("theme", "dark");
});
Такой подход позволяет проверять факт вызова метода без обращения к настоящему хранилищу.
Во многих случаях необходимо имитировать не только вызов функции, но и получение результата.
Функция приложения:
export async function loadTheme() {
return localforage.getItem("theme");
}
Настройка мока:
localforage.getItem.mockResolvedValue("dark");
Тест:
test("загружает тему", async () => {
const result = await loadTheme();
expect(result).toBe("dark");
});
Поскольку методы localForage возвращают Promise, рекомендуется
использовать именно mockResolvedValue.
Частая ситуация — проверка поведения приложения при отсутствии значения в хранилище.
Настройка:
localforage.getItem.mockResolvedValue(null);
Тестируемая функция:
export async function getLanguage() {
const language = await localforage.getItem("language");
return language || "en";
}
Проверка:
test("возвращает язык по умолчанию", async () => {
const result = await getLanguage();
expect(result).toBe("en");
});
Такой сценарий позволяет протестировать ветки логики, связанные с первоначальным запуском приложения.
Работа с хранилищем может завершаться ошибкой. Качественные тесты должны учитывать такие случаи.
Функция:
export async function saveUser(user) {
try {
await localforage.setItem("user", user);
return true;
} catch {
return false;
}
}
Настройка мока:
localforage.setItem.mockRejectedValue(
new Error("Storage error")
);
Тест:
test("обрабатывает ошибку сохранения", async () => {
const result = await saveUser({ id: 1 });
expect(result).toBe(false);
});
Использование mockRejectedValue позволяет воспроизводить
исключительные ситуации без модификации реального хранилища.
Для сложных тестов бывает недостаточно фиксированных ответов. В таких случаях удобно реализовать память в оперативной памяти.
Пример:
const storage = new Map();
const localforageMock = {
async setItem(key, value) {
storage.set(key, value);
return value;
},
async getItem(key) {
return storage.get(key);
},
async removeItem(key) {
storage.delete(key);
},
async clear() {
storage.clear();
}
};
Теперь мок начинает вести себя практически как настоящий localForage.
Тест:
await localforageMock.setItem(
"user",
{ name: "Alex" }
);
const user =
await localforageMock.getItem("user");
expect(user.name).toBe("Alex");
Такой подход особенно полезен для интеграционных тестов уровня бизнес-логики.
Одной из главных проблем является накопление данных между тестами.
Неправильно:
const storage = new Map();
Если очистка не выполняется, результаты тестов начинают зависеть друг от друга.
Корректный вариант:
beforeEach(() => {
storage.clear();
});
Либо:
afterEach(() => {
storage.clear();
});
Изоляция тестов является обязательным требованием для стабильного тестового набора.
Jest поддерживает автоматическое создание моков.
Пример:
jest.mock("localforage");
После этого каждый метод становится мок-функцией.
Настройка:
localforage.getItem.mockResolvedValue("dark");
Преимущества:
Недостатки:
В крупных проектах удобно создавать централизованные моки.
Структура:
src/
tests/
__mocks__/
localforage.js
Файл мока:
const storage = new Map();
export default {
async setItem(key, value) {
storage.set(key, value);
return value;
},
async getItem(key) {
return storage.get(key);
},
async removeItem(key) {
storage.delete(key);
},
async clear() {
storage.clear();
}
};
После подключения:
jest.mock("localforage");
Jest автоматически использует файл из каталога
__mocks__.
Это обеспечивает единое поведение во всех тестах проекта.
Многие приложения используют несколько независимых экземпляров localForage.
Пример:
const settingsStorage =
localforage.createInstance({
name: "settings"
});
В тестовой среде необходимо мокировать и этот механизм.
Пример:
const createInstance = jest.fn(() => ({
getItem: jest.fn(),
setItem: jest.fn(),
removeItem: jest.fn(),
clear: jest.fn()
}));
Проверка:
expect(
localforage.createInstance
).toHaveBeenCalled();
Без такого мока код может завершаться ошибкой ещё до выполнения тестируемой логики.
Иногда требуется отдельное состояние для каждого экземпляра.
Пример реализации:
function createStorage() {
const data = new Map();
return {
async getItem(key) {
return data.get(key);
},
async setItem(key, value) {
data.set(key, value);
return value;
}
};
}
Использование:
const localforageMock = {
createInstance() {
return createStorage();
}
};
Каждый вызов будет получать собственное независимое хранилище.
Мокирование удобно для анализа поведения приложения.
Пример:
expect(
localforage.getItem
).toHaveBeenCalledTimes(1);
Проверка аргументов:
expect(
localforage.getItem
).toHaveBeenCalledWith("user");
Проверка последовательности:
expect(
localforage.setItem.mock.calls
).toEqual([
["token", "123"],
["user", { id: 1 }]
]);
Такие проверки помогают контролировать корректность работы слоя хранения данных.
Метод iterate имеет особую сигнатуру и требует отдельной
реализации.
Пример мока:
async function iterate(callback) {
const data = {
a: 1,
b: 2,
c: 3
};
let index = 1;
for (const [key, value] of Object.entries(data)) {
callback(value, key, index++);
}
}
Тест:
const result = [];
await localforage.iterate((value) => {
result.push(value);
});
expect(result).toEqual([1, 2, 3]);
Такой мок позволяет воспроизводить поведение обхода коллекции.
Пример реализации:
const storage = new Map();
async function keys() {
return [...storage.keys()];
}
Проверка:
await localforage.setItem("a", 1);
await localforage.setItem("b", 2);
expect(
await localforage.keys()
).toEqual(["a", "b"]);
Это полезно при тестировании механизмов синхронизации и очистки данных.
Современные версии localForage поддерживают удаление экземпляра хранилища.
Упрощённый вариант:
async function dropInstance() {
storage.clear();
}
Проверка:
await localforage.setItem("user", 1);
await localforage.dropInstance();
expect(
await localforage.getItem("user")
).toBeUndefined();
Такой сценарий может использоваться при тестировании выхода пользователя из системы или сброса приложения.
Неправильно:
getItem() {
return "value";
}
Правильно:
async getItem() {
return "value";
}
Или:
getItem() {
return Promise.resolve("value");
}
Нередко мок содержит только один метод:
{
getItem: jest.fn()
}
Однако приложение может использовать:
localforage.createInstance()
или
localforage.clear()
В результате тесты падают не из-за ошибки бизнес-логики, а из-за неполного мока.
Проблема:
const storage = new Map();
без очистки между тестами.
Следствие:
test A → записал данные
test B → неожиданно получил данные
Подобные зависимости делают результаты нестабильными и затрудняют поиск реальных ошибок.
Многие наборы тестов проверяют только успешные операции:
mockResolvedValue(...)
Но не тестируют:
mockRejectedValue(...)
Из-за этого обработчики ошибок остаются непроверенными, хотя именно они часто становятся источником проблем в производственной среде.
Для небольших модулей обычно достаточно простых функций
jest.fn() с заранее заданными результатами.
Для тестирования сервисов хранения и бизнес-логики более удобны
in-memory реализации на основе Map, позволяющие
воспроизводить реальные операции чтения, записи и удаления.
Для крупных проектов наиболее эффективным вариантом становится
централизованный мок в каталоге __mocks__, поддерживающий
основные методы localForage, изоляцию данных между тестами, создание
экземпляров через createInstance и возможность искусственно
генерировать ошибки. Такой подход обеспечивает предсказуемое поведение
тестовой среды и позволяет тестировать код независимо от особенностей
браузерных механизмов хранения данных.