概要
document.cookie を操作する getCookie / setCookie / deleteCookie ユーティリティ関数を純粋関数として実装し、Jest でユニットテストする。jsdom 環境では document.cookie が実際にセットされるため追加モック不要な点と、httpOnly クッキーが jsdom では操作できない点を示す。
インストール
npm install jest @types/jest ts-jest
実装
テスト対象のクッキーユーティリティ
// lib/cookie.ts
/** クッキー値を取得する */
export function getCookie(name: string): string | undefined {
const value = `; ${document.cookie}`;
const parts = value.split(`; ${encodeURIComponent(name)}=`);
if (parts.length === 2) {
const raw = parts.pop()?.split(";").shift();
return raw !== undefined ? decodeURIComponent(raw) : undefined;
}
return undefined;
}
/** クッキーをセットする */
export function setCookie(
name: string,
value: string,
options: {
maxAge?: number;
path?: string;
sameSite?: "Strict" | "Lax" | "None";
} = {}
): void {
const { maxAge, path = "/", sameSite = "Lax" } = options;
let cookie = `${encodeURIComponent(name)}=${encodeURIComponent(value)}`;
if (maxAge !== undefined) cookie += `; max-age=${maxAge}`;
cookie += `; path=${path}`;
cookie += `; SameSite=${sameSite}`;
document.cookie = cookie;
}
/** クッキーを削除する(max-age=0 で上書き) */
export function deleteCookie(name: string, path = "/"): void {
document.cookie = `${encodeURIComponent(name)}=; max-age=0; path=${path}`;
}
基本的なテスト
// lib/cookie.test.ts
import { getCookie, setCookie, deleteCookie } from "./cookie";
describe("getCookie", () => {
beforeEach(() => {
// jsdom の cookie をリセット
document.cookie.split(";").forEach((c) => {
const name = c.trim().split("=")[0];
document.cookie = `${name}=; max-age=0; path=/`;
});
});
it("存在するクッキーの値を返す", () => {
document.cookie = "theme=dark; path=/";
expect(getCookie("theme")).toBe("dark");
});
it("存在しないクッキーは undefined を返す", () => {
expect(getCookie("nonexistent")).toBeUndefined();
});
it("複数のクッキーがある場合に正しい値を返す", () => {
document.cookie = "a=1; path=/";
document.cookie = "b=2; path=/";
expect(getCookie("a")).toBe("1");
expect(getCookie("b")).toBe("2");
});
});
describe("setCookie / deleteCookie", () => {
beforeEach(() => {
document.cookie.split(";").forEach((c) => {
const name = c.trim().split("=")[0];
document.cookie = `${name}=; max-age=0; path=/`;
});
});
it("setCookie でクッキーをセットできる", () => {
setCookie("lang", "ja");
expect(getCookie("lang")).toBe("ja");
});
it("deleteCookie で削除するとクッキーが取得できなくなる", () => {
setCookie("lang", "ja");
deleteCookie("lang");
expect(getCookie("lang")).toBeUndefined();
});
it("値に特殊文字が含まれても往復変換できる", () => {
setCookie("filter", "a=1&b=2");
expect(getCookie("filter")).toBe("a=1&b=2");
});
});
document.cookie を Object.defineProperty でモックする場合
jsdom の document.cookie は読み書きできるが、Secure や HttpOnly を含む完全なモックが必要なときは Object.defineProperty を使う。
// テスト内でのモック例
let cookieStore = "";
beforeEach(() => {
cookieStore = "";
Object.defineProperty(document, "cookie", {
get: () => cookieStore,
set: (val: string) => {
const [pair] = val.split(";");
const [key, value] = pair.split("=");
if (val.includes("max-age=0")) {
cookieStore = cookieStore
.split("; ")
.filter((c) => !c.startsWith(`${key}=`))
.join("; ");
} else {
const existing = cookieStore
.split("; ")
.filter((c) => !c.startsWith(`${key}=`));
cookieStore = [...existing, `${key}=${value}`].filter(Boolean).join("; ");
}
},
configurable: true,
});
});
ポイント
- jsdom 環境では
document.cookieへの書き込みが実際に機能するため、getCookie/setCookieの基本テストは追加モック不要 beforeEachでクッキーをリセットする処理はdocument.cookie.split(";")ループで行う。document.cookie = ""は動作しないencodeURIComponent/decodeURIComponentを通す往復テストで、特殊文字(&=+など)が安全に扱われることを確認する- jsdom では
Secureフラグ付きクッキーの書き込みが非 HTTPS 環境で無視される場合がある。Secureを含むロジックはObject.definePropertyモックで制御する httpOnlyクッキーは JavaScript から読み書きできない。Next.js のcookies()を使うサーバーサイドの cookie は別途next/headersのモックが必要になる