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

Jest で URLSearchParams を使った URL クエリ変換ロジックをテストする

URLSearchParams を操作するフィルタクエリ生成・パース関数を Jest でユニットテストする例。jsdom 環境での URLSearchParams の挙動とエッジケースの検証パターン。

nextjstestingsearch-filterjest

対応バージョン

nextjs 15react 19jest 29

前提環境

Jest の基本的なテスト記述と URLSearchParams API の基本を理解していること

概要

フィルタクエリを組み立てる buildFilterQuery とクエリ文字列を解析する parseFilterParams を純粋関数として実装し、Jest でユニットテストする。jsdom 環境で URLSearchParams がそのまま使える点と、空文字・特殊文字・複数値などのエッジケースを網羅する検証パターンを示す。

インストール

npm install jest @types/jest ts-jest

実装

テスト対象の純粋関数

// lib/filterQuery.ts

export type FilterParams = {
  q?: string;
  framework?: string;
  category?: string;
  difficulty?: string;
  page?: number;
};

/** FilterParams を URL クエリ文字列に変換する */
export function buildFilterQuery(params: FilterParams): string {
  const sp = new URLSearchParams();

  if (params.q) sp.set("q", params.q);
  if (params.framework) sp.set("framework", params.framework);
  if (params.category) sp.set("category", params.category);
  if (params.difficulty) sp.set("difficulty", params.difficulty);
  if (params.page && params.page > 1) sp.set("page", String(params.page));

  return sp.toString();
}

/** URL クエリ文字列を FilterParams に変換する */
export function parseFilterParams(search: string): FilterParams {
  const sp = new URLSearchParams(search);
  const result: FilterParams = {};

  const q = sp.get("q");
  if (q) result.q = q;

  const framework = sp.get("framework");
  if (framework) result.framework = framework;

  const category = sp.get("category");
  if (category) result.category = category;

  const difficulty = sp.get("difficulty");
  if (difficulty) result.difficulty = difficulty;

  const page = sp.get("page");
  const pageNum = page ? parseInt(page, 10) : NaN;
  if (!isNaN(pageNum) && pageNum > 0) result.page = pageNum;

  return result;
}

基本的なテスト

// lib/filterQuery.test.ts
import { buildFilterQuery, parseFilterParams } from "./filterQuery";

describe("buildFilterQuery", () => {
  it("すべてのパラメータを含むクエリを生成する", () => {
    const result = buildFilterQuery({
      q: "Next.js",
      framework: "nextjs",
      category: "routing",
      difficulty: "beginner",
      page: 2,
    });
    const sp = new URLSearchParams(result);
    expect(sp.get("q")).toBe("Next.js");
    expect(sp.get("framework")).toBe("nextjs");
    expect(sp.get("category")).toBe("routing");
    expect(sp.get("difficulty")).toBe("beginner");
    expect(sp.get("page")).toBe("2");
  });

  it("空のオブジェクトを渡すと空文字を返す", () => {
    expect(buildFilterQuery({})).toBe("");
  });

  it("page が 1 のときはクエリに含めない", () => {
    const result = buildFilterQuery({ framework: "nextjs", page: 1 });
    expect(new URLSearchParams(result).has("page")).toBe(false);
  });

  it("q が空文字のときはクエリに含めない", () => {
    const result = buildFilterQuery({ q: "", framework: "nextjs" });
    expect(new URLSearchParams(result).has("q")).toBe(false);
  });
});

describe("parseFilterParams", () => {
  it("クエリ文字列を FilterParams に変換する", () => {
    const result = parseFilterParams("q=Next.js&framework=nextjs&page=3");
    expect(result).toEqual({ q: "Next.js", framework: "nextjs", page: 3 });
  });

  it("空文字を渡すと空オブジェクトを返す", () => {
    expect(parseFilterParams("")).toEqual({});
  });

  it("存在しないキーは undefined のまま", () => {
    const result = parseFilterParams("q=hello");
    expect(result.framework).toBeUndefined();
    expect(result.category).toBeUndefined();
  });

  it("page が数値でない場合は無視する", () => {
    const result = parseFilterParams("page=abc");
    expect(result.page).toBeUndefined();
  });

  it("page が 0 以下の場合は無視する", () => {
    const result = parseFilterParams("page=0");
    expect(result.page).toBeUndefined();
  });
});

エッジケース: 特殊文字と日本語

describe("buildFilterQuery / parseFilterParams — 特殊文字", () => {
  it("日本語キーワードを往復変換できる", () => {
    const query = buildFilterQuery({ q: "認証フロー" });
    const parsed = parseFilterParams(query);
    expect(parsed.q).toBe("認証フロー");
  });

  it("スペースを含むキーワードを往復変換できる", () => {
    const query = buildFilterQuery({ q: "Next.js routing" });
    const parsed = parseFilterParams(query);
    expect(parsed.q).toBe("Next.js routing");
  });

  it("& や = を含む文字列を正しくエンコード・デコードする", () => {
    const query = buildFilterQuery({ q: "a=1&b=2" });
    const parsed = parseFilterParams(query);
    expect(parsed.q).toBe("a=1&b=2");
  });
});

エッジケース: ページリセット

describe("クエリ更新時のページリセット", () => {
  it("フィルタを変更した場合は page を除去する", () => {
    const current = "q=hello&framework=nextjs&page=3";
    const sp = new URLSearchParams(current);
    sp.set("framework", "react");
    sp.delete("page");
    const result = parseFilterParams(sp.toString());
    expect(result.page).toBeUndefined();
    expect(result.framework).toBe("react");
  });
});

ポイント

  • buildFilterQueryparseFilterParams を pure function として実装することで、Next.js のルーターや DOM に依存せず Jest でそのままテストできる
  • jsdom 環境では URLSearchParams がグローバルに利用可能なため、追加の mock 設定は不要
  • sp.toString() で生成した文字列を new URLSearchParams(result) で再パースして検証することで、エンコード方式の差異に関係なく意味的な等価性を確認できる
  • 空文字・undefined・ゼロ・負数といった境界値をテストすることで、フィルタ条件なしの状態と有効な条件の両方を網羅できる
  • 特殊文字・日本語・スペースは往復変換テスト(build → parse → 元の値と等しい)で一括検証するのが簡潔
  • ページリセットロジックは「クエリ文字列を URLSearchParams で操作してから delete("page")」というパターンを純粋関数的にテストできる

注意点

jest-fetch-mock-test は fetch グローバルのモック。jest-localstorage-mock はストレージのモック。これは URL クエリ文字列を操作する純粋関数(buildFilterQuery / parseFilterParams 等)のユニットテストに特化。URLSearchParams の jsdom 環境での挙動も示す。

関連サンプル