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

При разработке приложений, использующих localForage, значительная часть логики зависит от операций чтения и записи данных. В реальном окружении библиотека работает поверх IndexedDB, WebSQL или LocalStorage, однако во время выполнения юнит-тестов использование настоящих браузерных хранилищ часто приводит к проблемам:

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

По этой причине в юнит-тестировании широко применяется мокирование 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 позволяет воспроизводить исключительные ситуации без модификации реального хранилища.


Создание собственного in-memory хранилища

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

Пример:

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 поддерживает автоматическое создание моков.

Пример:

jest.mock("localforage");

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

Настройка:

localforage.getItem.mockResolvedValue("dark");

Преимущества:

  • минимум кода;
  • быстрое создание тестов;
  • удобная проверка вызовов.

Недостатки:

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

Использование директории mocks

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

Структура:

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__.

Это обеспечивает единое поведение во всех тестах проекта.


Мокирование createInstance

Многие приложения используют несколько независимых экземпляров 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

Метод 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]);

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


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

Пример реализации:

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"]);

Это полезно при тестировании механизмов синхронизации и очистки данных.


Мокирование dropInstance

Современные версии localForage поддерживают удаление экземпляра хранилища.

Упрощённый вариант:

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

Проверка:

await localforage.setItem("user", 1);

await localforage.dropInstance();

expect(
    await localforage.getItem("user")
).toBeUndefined();

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


Типичные ошибки при мокировании

Использование синхронных методов вместо Promise

Неправильно:

getItem() {
    return "value";
}

Правильно:

async getItem() {
    return "value";
}

Или:

getItem() {
    return Promise.resolve("value");
}

Частичное покрытие API

Нередко мок содержит только один метод:

{
    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 и возможность искусственно генерировать ошибки. Такой подход обеспечивает предсказуемое поведение тестовой среды и позволяет тестировать код независимо от особенностей браузерных механизмов хранения данных.