Поведение клиентских хранилищ данных в браузере отличается высокой вариативностью: IndexedDB, WebSQL и localStorage обладают разными ограничениями, особенностями реализации и режимами отказа. Библиотека localForage абстрагирует эти различия, но не устраняет необходимость учитывать ошибки нижележащего слоя.
Тестирование сценариев отказа становится критически важным в следующих случаях:
Реальные ошибки хранилища трудно воспроизвести стабильно, поэтому применяется искусственная эмуляция отказов на уровне драйверов и API.
localForage возвращает промисы, которые могут завершаться ошибкой при взаимодействии с хранилищем. Основные категории:
Ситуация, при которой браузер запрещает запись из-за превышения лимита:
QuotaExceededErrorNS_ERROR_DOM_QUOTA_REACHED (Firefox-специфика)Поведение часто зависит от драйвера и объёма данных.
Возникают при ограничениях окружения:
SecurityError при отключённом IndexedDB;Связаны с IndexedDB:
TransactionInactiveError;InvalidStateError;localForage использует сериализацию данных перед записью:
Наиболее прямолинейный способ — замена методов экземпляра:
import localforage from "localforage";
const store = localforage.createInstance({
name: "testStore"
});
store.setItem = () => {
return Promise.reject(new Error("Simulated write failure"));
};
Такой подход используется для проверки реакции бизнес-логики на отказ записи.
localForage позволяет работать через драйверы. Можно перехватывать низкоуровневые операции:
const failingDriver = {
_driver: "mockDriver",
_initStorage: () => Promise.resolve(),
setItem: () => Promise.reject(new Error("IndexedDB unavailable")),
getItem: () => Promise.resolve(null),
removeItem: () => Promise.resolve(),
clear: () => Promise.resolve()
};
localforage.defineDriver(failingDriver);
localforage.setDriver("mockDriver");
Такой способ обеспечивает централизованное управление поведением хранилища.
Для имитации переполнения квоты используется генерация DOMException:
function createQuotaError() {
const error = new DOMException(
"Quota exceeded",
"QuotaExceededError"
);
return error;
}
store.setItem = () => Promise.reject(createQuotaError());
Подобный подход позволяет тестировать стратегии очистки кэша и деградации функциональности.
Более гибкий механизм основан на Proxy-обёртке:
function withFailureSimulation(target, shouldFail) {
return new Proxy(target, {
get(obj, prop) {
if (shouldFail(prop)) {
return () => Promise.reject(new Error("Injected failure"));
}
return obj[prop];
}
});
}
const wrapped = withFailureSimulation(store, (prop) => prop === "setItem");
Преимущество подхода — выборочная эмуляция отказов по операциям.
В тестовых средах часто используется полное мокирование модуля:
jest.mock("localforage", () => {
return {
createInstance: () => ({
setItem: jest.fn(() => Promise.reject(new Error("DB error"))),
getItem: jest.fn(() => Promise.resolve(null)),
removeItem: jest.fn(() => Promise.resolve()),
clear: jest.fn(() => Promise.resolve())
})
};
});
Такой способ обеспечивает предсказуемость тестов и изоляцию от реализации браузера.
Для проверки устойчивости к нестабильным соединениям используется стохастическое поведение:
let counter = 0;
store.setItem = () => {
counter++;
if (counter % 2 === 0) {
return Promise.reject(new Error("Intermittent failure"));
}
return Promise.resolve();
};
Такой сценарий моделирует нестабильность IndexedDB в реальных условиях.
Эмуляция медленных операций позволяет тестировать race conditions:
store.getItem = () => {
return new Promise((resolve) => {
setTimeout(() => resolve("value"), 2000);
});
};
Комбинация задержек и отказов выявляет проблемы конкурентного доступа.
Эмуляция ошибок часто используется совместно с проверкой retry-механизмов.
async function writeWithRetry(key, value, retries = 3) {
for (let i = 0; i < retries; i++) {
try {
await store.setItem(key, value);
return true;
} catch (e) {
if (i === retries - 1) throw e;
}
}
}
Тестирование подобной логики требует детерминированных отказов на первых итерациях и успеха на последней.
При ошибках записи часто требуется очистка состояния:
store.setItem = async () => {
await store.clear();
throw new Error("Critical storage failure");
};
Такой сценарий используется для проверки fallback-логики приложения.
Эмуляция ошибок требует строгого контроля состояния хранилища:
Пример восстановления:
let originalSetItem;
beforeEach(() => {
originalSetItem = store.setItem;
});
afterEach(() => {
store.setItem = originalSetItem;
});
В некоторых случаях создаётся полностью изолированный драйвер:
const memoryDriver = {
_driver: "memory",
store: {},
setItem(key, value) {
this.store[key] = value;
return Promise.resolve(value);
},
getItem(key) {
return Promise.resolve(this.store[key] || null);
},
clear() {
this.store = {};
return Promise.resolve();
}
};
Поверх такого драйвера легко накладывать слой ошибок, не затрагивая реальные браузерные API.
Реальные системы редко сталкиваются с одиночными ошибками. Более реалистичная модель включает комбинации:
Пример комбинированного поведения:
store.setItem = () => {
if (Math.random() < 0.5) {
return Promise.reject(new Error("Random write failure"));
}
return Promise.resolve();
};
store.getItem = () => {
if (Math.random() < 0.3) {
return Promise.reject(new Error("Read failure"));
}
return Promise.resolve("data");
};
Эмуляция ошибок требует исключения случайности при прогоне тестов:
Math.random стабилизированным значением;Пример фиксации:
Math.random = () => 0.1;
Для глубокого тестирования применяется слой-обёртка:
function instrument(store) {
return {
setItem: async (k, v) => {
console.log("setItem", k);
return store.setItem(k, v);
}
};
}
На такой обёртке легко строить эмуляцию отказов, не изменяя исходную реализацию.
Моделирование полной недоступности localForage:
const brokenStorage = {
setItem: () => Promise.reject(new Error("Storage disabled")),
getItem: () => Promise.reject(new Error("Storage disabled")),
clear: () => Promise.reject(new Error("Storage disabled"))
};
Такой сценарий используется для проверки деградации функциональности до stateless-режима, когда данные обрабатываются только в памяти без персистентности.