概要
Jest の jsdom 環境には localStorage / sessionStorage の実装が含まれているが、テスト間でデータが混在したり、特定のエラーケースを再現するために spyOn でモックを差し替える必要がある。beforeEach でのクリアと spyOn による制御パターンを示す。
インストール
npm install --save-dev jest @types/jest ts-jest
実装
テスト対象のストレージユーティリティ
// lib/storage.ts
const THEME_KEY = "app_theme";
const AUTH_TOKEN_KEY = "auth_token";
export type Theme = "light" | "dark";
export function getTheme(): Theme {
try {
const stored = localStorage.getItem(THEME_KEY);
return stored === "dark" ? "dark" : "light";
} catch {
return "light";
}
}
export function setTheme(theme: Theme): void {
localStorage.setItem(THEME_KEY, theme);
}
export function clearTheme(): void {
localStorage.removeItem(THEME_KEY);
}
export function getAuthToken(): string | null {
return sessionStorage.getItem(AUTH_TOKEN_KEY);
}
export function setAuthToken(token: string): void {
sessionStorage.setItem(AUTH_TOKEN_KEY, token);
}
export function clearAuthToken(): void {
sessionStorage.removeItem(AUTH_TOKEN_KEY);
}
パターン 1: jsdom のデフォルト実装をそのまま使う
// lib/storage.test.ts
import { getTheme, setTheme, clearTheme, getAuthToken, setAuthToken } from "./storage";
describe("localStorage を使うテーマ関数", () => {
// テスト前にストレージをクリアして独立性を保つ
beforeEach(() => {
localStorage.clear();
sessionStorage.clear();
});
it("初期状態では light を返す", () => {
expect(getTheme()).toBe("light");
});
it("setTheme('dark') で dark が保存される", () => {
setTheme("dark");
expect(getTheme()).toBe("dark");
});
it("clearTheme 後は light に戻る", () => {
setTheme("dark");
clearTheme();
expect(getTheme()).toBe("light");
});
});
describe("sessionStorage を使うトークン関数", () => {
beforeEach(() => {
sessionStorage.clear();
});
it("未設定のとき null を返す", () => {
expect(getAuthToken()).toBeNull();
});
it("setAuthToken で保存したトークンを取得できる", () => {
setAuthToken("token-abc-123");
expect(getAuthToken()).toBe("token-abc-123");
});
});
パターン 2: spyOn でモックして例外ケースをテスト
// lib/storage-error.test.ts
import { getTheme, setTheme } from "./storage";
describe("localStorage が使用できない環境のフォールバック", () => {
let getItemSpy: jest.SpyInstance;
let setItemSpy: jest.SpyInstance;
afterEach(() => {
getItemSpy?.mockRestore();
setItemSpy?.mockRestore();
});
it("localStorage.getItem が例外を投げても light を返す", () => {
getItemSpy = jest
.spyOn(Storage.prototype, "getItem")
.mockImplementation(() => {
throw new DOMException("Access denied");
});
expect(getTheme()).toBe("light");
});
it("特定のキーにだけモックを適用する", () => {
getItemSpy = jest
.spyOn(Storage.prototype, "getItem")
.mockImplementation((key: string) => {
if (key === "app_theme") return "dark";
return null;
});
expect(getTheme()).toBe("dark");
});
});
パターン 3: カスタムフックのストレージ操作をテスト
// hooks/useTheme.ts
"use client";
import { useCallback, useEffect, useState } from "react";
import { getTheme, setTheme, type Theme } from "@/lib/storage";
export function useTheme() {
const [theme, setThemeState] = useState<Theme>("light");
useEffect(() => {
setThemeState(getTheme());
}, []);
const toggle = useCallback(() => {
const next: Theme = theme === "light" ? "dark" : "light";
setTheme(next);
setThemeState(next);
}, [theme]);
return { theme, toggle };
}
// hooks/useTheme.test.ts
import { renderHook, act } from "@testing-library/react";
import { useTheme } from "./useTheme";
describe("useTheme", () => {
beforeEach(() => {
localStorage.clear();
});
it("初期テーマは light", () => {
const { result } = renderHook(() => useTheme());
expect(result.current.theme).toBe("light");
});
it("toggle で dark に切り替わる", () => {
const { result } = renderHook(() => useTheme());
act(() => {
result.current.toggle();
});
expect(result.current.theme).toBe("dark");
expect(localStorage.getItem("app_theme")).toBe("dark");
});
it("toggle を 2 回呼ぶと light に戻る", () => {
const { result } = renderHook(() => useTheme());
act(() => result.current.toggle());
act(() => result.current.toggle());
expect(result.current.theme).toBe("light");
});
});
ポイント
- Jest の jsdom 環境では
localStorage/sessionStorageが実際に動作する(インメモリ実装)。beforeEach(() => localStorage.clear())でテスト間のデータ混在を防ぐ - 例外ケースや特定の返り値を制御したいときは
jest.spyOn(Storage.prototype, "getItem")を使う。Storage.prototypeへのモックはすべてのストレージインスタンスに適用される afterEach(() => spy.mockRestore())でモックを元に戻し、他のテストスイートに影響を与えないmockImplementation((key: string) => ...)で引数によって返り値を変えられる。特定のキーのみモックし、他は通常通り動作させる実装が可能renderHook+actでカスタムフックをテストする場合も、beforeEachでストレージをクリアするだけで追加の準備は不要。jsdom が内部でストレージを管理してくれる