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