Мок localStorage в Vitest
stubWebStorage подменяет localStorage или sessionStorage новым хранилищем в памяти на один тест. Начальные данные задаются в том же вызове, а всё сохранённое проверяется одним toEqual.
import { beforeEach, describe, expect, it } from 'vitest';
import { type WebStorageStub, stubWebStorage } from 'vitest-auto-spy/dom-stubs';
import { forgetUser, loadTheme, saveTheme } from './preferences';
describe('preferences', () => {
let local: WebStorageStub;
beforeEach(() => {
local = stubWebStorage('localStorage', { items: { theme: 'dark', token: 'abc' } });
});
it('reads the saved theme', () => {
expect(loadTheme()).toBe('dark');
});
it('writes the theme back', () => {
saveTheme('light');
expect(local.snapshot()).toEqual({ theme: 'light', token: 'abc' });
});
it('forgets the user in both storages', () => {
const session = stubWebStorage('sessionStorage', { items: { draft: '{}' } });
forgetUser();
expect(local.snapshot()).toEqual({ theme: 'dark' });
expect(session.snapshot()).toEqual({});
});
});Восстановление после каждого теста. Вызовите setupAutoSpy() один раз в сетап-файле (файл из setupFiles конфига Vitest). После каждого теста он восстанавливает исходное хранилище, и afterEach писать не нужно:
// vitest.setup.ts
import { setupAutoSpy } from 'vitest-auto-spy/setup';
setupAutoSpy();Без setupAutoSpy() восстановите хранилище сами:
import { afterEach } from 'vitest';
import { restoreMockedProps } from 'vitest-auto-spy';
afterEach(() => restoreMockedProps());Аргументы и что возвращается
stubWebStorage(key?, options?)
| Аргумент | Тип | По умолчанию | Смысл |
|---|---|---|---|
key | 'localStorage' | 'sessionStorage' | 'localStorage' | какой глобал подменить |
options.items | Record<string, string> | пусто | начальное содержимое; записывается через setItem |
options.view | object | null | document.defaultView | ещё один объект окна, куда поставить подмену; null — только на globalThis |
view нужен редко. По умолчанию подмена ставится на globalThis и ещё на document.defaultView (window из jsdom или happy-dom), если это другой объект. view меняет эту вторую цель:
- передайте другой объект окна, если код читает хранилище из него, например фейковый
window, который вы внедряете; - передайте
null, чтобы поставить подмену только наglobalThis; тогдаwindow.localStorageостанется хранилищем окружения.
Здесь и дальше хранилище окружения — настоящий localStorage, который даёт тестовое окружение (jsdom или happy-dom).
Начальные значения задавайте через items. getItem — настоящий метод, а не спай. Если всё же нужен принудительный ответ, поставьте на него спай: vi.spyOn(local.storage, 'getItem').mockReturnValue('dark').
Возвращается WebStorageStub:
| Поле | Что это |
|---|---|
storage | установленный объект Storage; его возвращает глобал до конца теста |
snapshot() | копия текущего содержимого хранилища в виде обычного объекта |
Как ведёт себя подмена
- Настоящий
Storage.getItem,setItem,removeItem,clear,keyиlengthработают как в браузере. Число, записанное черезsetItem, читается строкой. Отсутствующий ключ даётnull, а неundefined. Самописный фейк часто ошибается в этом и даёт тесту пройти там, где в браузере код бы упал. - Заполняется так же, как реальный код.
itemsзаписываются черезsetItem, поэтому значения приводятся к строкам тем же способом. - Проверяется как данные.
snapshot()возвращает копию, и одинtoEqualпроверяет всё хранилище. - Оба имени. В
jsdomиhappy-domобъектwindowможет не совпадать сglobalThis. Подмена ставится на оба, поэтомуlocalStorageиwindow.localStorageвозвращают одну и ту же подмену. - Любое окружение. Работает и в тестовом окружении
node, где своего хранилища нет.
Проверить, что был конкретный вызов
Обычно тест проверяет, что сохранено, а не как. Но иногда сам вызов и есть поведение: кеш не должен писать дважды, ключ нужно удалить, а не перезаписать. Тогда ставьте спай на установленное хранилище, а не на Storage.prototype:
it('writes the theme once', () => {
const setItem = vi.spyOn(local.storage, 'setItem');
saveTheme('dark');
expect(setItem).toHaveBeenCalledTimes(1);
expect(setItem).toHaveBeenCalledWith('theme', 'dark');
});Спай видит каждый вызов вашего кода и не трогает остальные объекты Storage.
Проверить ошибку «хранилище переполнено»
У подмены нет лимита размера, поэтому сама setItem никогда не бросает ошибку. Заставьте её:
vi.spyOn(local.storage, 'setItem').mockImplementation(() => {
throw new DOMException('full', 'QuotaExceededError');
});Сравнение с другими подходами
| Подход | Минус | Когда подходит |
|---|---|---|
Самописный фейк на Map + vi.stubGlobal | Около двадцати строк в каждом проекте; нужно не забыть восстановление и правила приведения к строке | Редко: stubWebStorage — это и есть такой фейк, со встроенным восстановлением |
Пакет …-localstorage-mock в setupFiles | Одно хранилище на весь файл: что записал один тест, читает следующий, если не очистить | Тестам нужно только, чтобы хранилище существовало |
vi.spyOn(Storage.prototype, 'setItem') | Меняет все объекты Storage; зависит от того, работает ли хранилище окружения | Нужно проверить вызовы на собственном хранилище окружения |
stubWebStorage | Один импорт из vitest-auto-spy/dom-stubs | Спека задаёт содержимое, читает его или должна начинать с пустого |
Если localStorage сломан на Node 25 и новее
В окружении jsdom или happy-dom в Vitest на Node 25+ localStorage может быть сломан ещё до запуска спек:
- Node 25:
setItem is not a function; - Node 26:
localStorageравенundefined.
Причина: в Node 25 появился собственный глобальный localStorage. Vitest копирует глобалы DOM-окружения на глобальный объект теста, но пропускает имена, которые уже есть. Поэтому остаётся версия из Node, а рабочее хранилище из jsdom или happy-dom так и не приходит. setupAutoSpy() чинит это по умолчанию. Подробности — в разделе Web Storage, которое раннер так и не передал. stubWebStorage работает в любом случае: он заменяет то, что есть.
Ограничения
- Нет доступа к данным через свойства.
localStorage.tokenиObject.keys(localStorage)не видят данные. ИспользуйтеgetItemиsnapshot(). - Нет события
storage. Другим окнам ничего не отправляется: в тесте их нет. - Нет лимита размера. См. Проверить ошибку «хранилище переполнено».
Смотрите также
stubWebStorageв справочнике по сетапу: API рядом с исправлением для Node 25.- Заглушки observer-ов: тот же подход «поставить и вернуть» для
IntersectionObserver,ResizeObserverиMutationObserver.