Эмуляция ошибок хранилища в тестах

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

Тестирование сценариев отказа становится критически важным в следующих случаях:

  • переполнение квоты хранилища;
  • временная недоступность IndexedDB;
  • повреждение базы данных;
  • запрет доступа в приватных режимах браузера;
  • ошибки сериализации данных;
  • сбои транзакций;
  • блокировка доступа политиками безопасности (SecurityError).

Реальные ошибки хранилища трудно воспроизвести стабильно, поэтому применяется искусственная эмуляция отказов на уровне драйверов и API.


Типы ошибок, возникающих в localForage

localForage возвращает промисы, которые могут завершаться ошибкой при взаимодействии с хранилищем. Основные категории:

Ошибки квоты

Ситуация, при которой браузер запрещает запись из-за превышения лимита:

  • QuotaExceededError
  • NS_ERROR_DOM_QUOTA_REACHED (Firefox-специфика)

Поведение часто зависит от драйвера и объёма данных.


Ошибки доступа

Возникают при ограничениях окружения:

  • SecurityError при отключённом IndexedDB;
  • блокировка в sandbox iframe;
  • ограничения корпоративных политик браузера.

Ошибки транзакций

Связаны с IndexedDB:

  • TransactionInactiveError;
  • InvalidStateError;
  • сбои commit/abort транзакций.

Ошибки сериализации

localForage использует сериализацию данных перед записью:

  • невозможность сериализовать циклические структуры;
  • ошибки structured clone algorithm;
  • повреждённые данные при кастомных драйверах.

Неожиданные IO-ошибки

  • потеря соединения с IndexedDB backend;
  • повреждение object store;
  • ошибки миграции схемы.

Подходы к эмуляции ошибок

Переопределение методов 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");

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


Эмуляция QuotaExceededError

Для имитации переполнения квоты используется генерация 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

В тестовых средах часто используется полное мокирование модуля:

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-логики приложения.


Изоляция состояния между тестами

Эмуляция ошибок требует строгого контроля состояния хранилища:

  • сброс драйвера после каждого теста;
  • повторная инициализация localForage;
  • очистка in-memory storage;
  • восстановление оригинальных методов.

Пример восстановления:

let originalSetItem;

beforeEach(() => {
  originalSetItem = store.setItem;
});

afterEach(() => {
  store.setItem = originalSetItem;
});

Использование in-memory драйверов для тестирования

В некоторых случаях создаётся полностью изолированный драйвер:

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.


Комбинированные сценарии отказов

Реальные системы редко сталкиваются с одиночными ошибками. Более реалистичная модель включает комбинации:

  • частичная потеря данных + quota error;
  • задержка записи + сбой транзакции;
  • успешное чтение + ошибка записи;
  • нестабильный getItem при стабильном setItem.

Пример комбинированного поведения:

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-режима, когда данные обрабатываются только в памяти без персистентности.