レビュー済み·難易度: 中級·更新: 2026-04-18

Jest で localStorage / sessionStorage をモックしてフロントエンドロジックをテストする

jest の beforeEach でストレージをクリア・spyOn でモック化し、localStorage や sessionStorage を使うカスタムフックや関数を単体テストする例。

nextjstestingjest

対応バージョン

nextjs 15react 19jest 29

前提環境

Jest の基本的な書き方と localStorage API の基本を理解していること

概要

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 が内部でストレージを管理してくれる

注意点

jest-fetch-mock-test は fetch グローバルのモック。jest-mock-module はモジュール依存の差し替え。jest-fake-timer-test は時間制御。これは localStorage / sessionStorage の spyOn モックでストレージ依存ロジックをテストするパターンに特化。jsdom 環境のデフォルト実装の挙動も示す。

関連サンプル