Асинхронная природа localForage оказывает непосредственное влияние на
организацию тестов. В отличие от обычного localStorage, где
большинство операций выполняется синхронно, localForage предоставляет
API на основе Promise и может использовать различные механизмы хранения
данных, включая IndexedDB, WebSQL и LocalStorage.
При тестировании необходимо учитывать несколько особенностей:
Основная цель тестирования заключается в проверке корректности взаимодействия приложения с локальным хранилищем независимо от используемого драйвера.
Наиболее распространённая категория тестов связана с сохранением и извлечением данных.
Пример функции:
import localforage from "localforage";
export async function saveUser(user) {
await localforage.setItem("user", user);
}
export async function loadUser() {
return localforage.getItem("user");
}
Тест с использованием Jest:
import localforage from "localforage";
import { saveUser, loadUser } from "./userStorage";
describe("user storage", () => {
beforeEach(async () => {
await localforage.clear();
});
test("should save user", async () => {
const user = {
id: 1,
name: "John"
};
await saveUser(user);
const stored = await localforage.getItem("user");
expect(stored).toEqual(user);
});
test("should load user", async () => {
const user = {
id: 1,
name: "John"
};
await localforage.setItem("user", user);
const result = await loadUser();
expect(result).toEqual(user);
});
});
Здесь используется ключевой принцип тестирования асинхронного кода —
ожидание завершения операций через await.
Современные тестовые фреймворки позволяют объявлять тестовые сценарии асинхронными.
Правильный вариант:
test("loads data", async () => {
await localforage.setItem("theme", "dark");
const theme = await localforage.getItem("theme");
expect(theme).toBe("dark");
});
Неправильный вариант:
test("loads data", () => {
localforage.setItem("theme", "dark");
const theme = localforage.getItem("theme");
expect(theme).toBe("dark");
});
Во втором примере сравнивается Promise вместо результата операции, что приводит к ошибкам и ложным результатам тестирования.
Некоторые проекты предпочитают стиль тестирования без
async/await.
Пример:
test("stores value", () => {
return localforage
.setItem("language", "ru")
.then(() => localforage.getItem("language"))
.then(value => {
expect(value).toBe("ru");
});
});
Jest ожидает завершения Promise и корректно обрабатывает результат теста.
Удаление является важной частью работы с локальным хранилищем.
Функция:
export async function removeUser() {
await localforage.removeItem("user");
}
Тест:
test("removes user", async () => {
await localforage.setItem("user", {
id: 1
});
await removeUser();
const user = await localforage.getItem("user");
expect(user).toBeNull();
});
Тест подтверждает, что после удаления данные больше недоступны.
Метод clear() удаляет все записи текущего экземпляра
localForage.
Пример:
test("clears storage", async () => {
await localforage.setItem("a", 1);
await localforage.setItem("b", 2);
await localforage.clear();
const a = await localforage.getItem("a");
const b = await localforage.getItem("b");
expect(a).toBeNull();
expect(b).toBeNull();
});
При использовании метода keys() удобно тестировать
структуру хранилища.
test("returns keys", async () => {
await localforage.setItem("user", {});
await localforage.setItem("settings", {});
const keys = await localforage.keys();
expect(keys).toContain("user");
expect(keys).toContain("settings");
});
Метод length() возвращает число сохранённых
элементов.
test("counts records", async () => {
await localforage.clear();
await localforage.setItem("a", 1);
await localforage.setItem("b", 2);
const count = await localforage.length();
expect(count).toBe(2);
});
Такие проверки особенно полезны при реализации кэшей.
Асинхронные операции могут завершаться исключениями.
Рассмотрим функцию:
export async function saveSettings(settings) {
try {
await localforage.setItem("settings", settings);
return true;
} catch {
return false;
}
}
Тест ошибки:
import localforage from "localforage";
test("handles storage errors", async () => {
jest
.spyOn(localforage, "setItem")
.mockRejectedValue(new Error("Storage error"));
const result = await saveSettings({
theme: "dark"
});
expect(result).toBe(false);
});
Здесь происходит искусственная генерация ошибки через мок.
Если функция не обрабатывает ошибку самостоятельно, необходимо проверить факт возникновения исключения.
Функция:
export async function writeData(data) {
return localforage.setItem("data", data);
}
Тест:
test("throws storage error", async () => {
jest
.spyOn(localforage, "setItem")
.mockRejectedValue(new Error("Disk full"));
await expect(
writeData({})
).rejects.toThrow("Disk full");
});
Конструкция rejects.toThrow() предназначена специально
для Promise-ошибок.
Каждый тест должен работать независимо от других.
Типичная подготовка окружения:
beforeEach(async () => {
await localforage.clear();
});
Иногда дополнительно используется:
afterEach(() => {
jest.restoreAllMocks();
});
Это предотвращает влияние моков на последующие тестовые сценарии.
Использование глобального экземпляра может приводить к конфликтам между тестами.
Лучшей практикой считается создание собственного экземпляра:
const storage = localforage.createInstance({
name: "test-db"
});
Тест:
test("stores value in instance", async () => {
await storage.setItem("counter", 10);
const value = await storage.getItem("counter");
expect(value).toBe(10);
});
Такой подход облегчает изоляцию данных.
В модульных тестах зачастую не требуется реальное взаимодействие с IndexedDB.
Пример полного мока:
jest.mock("localforage", () => ({
setItem: jest.fn(),
getItem: jest.fn(),
removeItem: jest.fn(),
clear: jest.fn()
}));
Настройка поведения:
localforage.getItem.mockResolvedValue({
id: 10
});
Проверка вызова:
expect(localforage.getItem)
.toHaveBeenCalledWith("user");
Подобный подход делает тесты быстрыми и предсказуемыми.
Иногда требуется заменить только одну функцию.
jest.spyOn(localforage, "getItem")
.mockResolvedValue({
id: 100
});
Остальной API продолжает работать в штатном режиме.
Тесты могут контролировать корректность использования API localForage.
const spy = jest.spyOn(
localforage,
"setItem"
);
await saveUser({
id: 1
});
expect(spy).toHaveBeenCalledTimes(1);
Проверка аргументов:
expect(spy).toHaveBeenCalledWith(
"user",
{
id: 1
}
);
В сложных сценариях важно проверять порядок вызовов.
Пример функции:
export async function replaceUser(user) {
await localforage.removeItem("user");
await localforage.setItem("user", user);
}
Тест:
test("replaces user", async () => {
const removeSpy =
jest.spyOn(localforage, "removeItem");
const setSpy =
jest.spyOn(localforage, "setItem");
await replaceUser({
id: 5
});
expect(removeSpy).toHaveBeenCalled();
expect(setSpy).toHaveBeenCalled();
});
Подобные проверки гарантируют соблюдение бизнес-логики.
Асинхронность позволяет выполнять несколько запросов одновременно.
Функция:
export async function saveBatch(data) {
return Promise.all(
data.map(item =>
localforage.setItem(
item.key,
item.value
)
)
);
}
Тест:
test("saves multiple records", async () => {
const records = [
{
key: "a",
value: 1
},
{
key: "b",
value: 2
}
];
await saveBatch(records);
expect(
await localforage.getItem("a")
).toBe(1);
expect(
await localforage.getItem("b")
).toBe(2);
});
localForage часто используется для хранения сложных структур данных.
test("stores nested objects", async () => {
const data = {
user: {
id: 1,
profile: {
name: "John",
age: 30
}
},
permissions: [
"read",
"write"
]
};
await localforage.setItem(
"data",
data
);
const result =
await localforage.getItem("data");
expect(result).toEqual(data);
});
Тест подтверждает корректную сериализацию и десериализацию структуры.
Одним из преимуществ localForage является поддержка Blob, ArrayBuffer и TypedArray.
Пример:
test("stores binary data", async () => {
const bytes =
new Uint8Array([1, 2, 3, 4]);
await localforage.setItem(
"file",
bytes
);
const result =
await localforage.getItem("file");
expect(Array.from(result))
.toEqual([1, 2, 3, 4]);
});
Подобные проверки актуальны для офлайн-приложений, систем кэширования изображений и хранения файлов.
Интеграционные тесты проверяют взаимодействие между несколькими компонентами приложения и localForage.
Пример сценария:
await loginUser();
const cachedUser =
await localforage.getItem("user");
expect(cachedUser.id)
.toBeDefined();
Здесь тестируется не только работа localForage, но и корректность бизнес-процесса авторизации.
Наиболее частая ошибка:
localforage.setItem("a", 1);
expect(
await localforage.getItem("a")
).toBe(1);
Запись может ещё не завершиться к моменту чтения.
Правильный вариант:
await localforage.setItem("a", 1);
expect(
await localforage.getItem("a")
).toBe(1);
Если хранилище не очищается:
beforeEach(async () => {
await localforage.clear();
});
то один тест может влиять на другой.
После использования:
jest.spyOn(...)
необходимо выполнять:
jest.restoreAllMocks();
или
mockRestore();
Иначе поведение библиотеки будет изменено для последующих тестов.
Неправильно:
writeData({});
Правильно:
await writeData({});
или
return writeData({});
Только в этом случае тестовый фреймворк сможет корректно отследить завершение операции и возможные исключения.