テストを書いたことのあるプログラマーなら一度は「なんでテストが通らないんだ…?」と頭を抱えた経験があると思います。今回、私もまさにその状態に陥りました。それは、Next.js の Edge Runtime を使ったプロジェクトでのこと。Route Handler をテストしようとしたとき、あの忌々しい「is not a function
」エラーに出くわしました。このエラーを克服するために試行錯誤した経験を、ここで共有します。同じ問題に悩む方の助けになれば幸いです。
問題に直面したときの状況
私が取り組んでいたのは、Next.js の Route Handler のテストでした。特に Edge Runtime を使う際には、Node.js 標準の Response
と Next.js 独自の拡張 Response
の間で競合が起こることがあります。その結果、NextResponse.json()
を呼び出した瞬間に「is not a function
」というエラーが発生してしまいました。
さらに、Jest を使ったテスト環境では、モジュールの読み込み順や jest.mock()
の使い方次第で新たな問題が発生することがありました。このような細かいトラップがいくつも重なり、初めは手詰まり状態でした。
私が実践した解決策
いろいろ試してみた結果、以下の方法を組み合わせることで問題を解決できました。それぞれのアプローチを詳しく説明します。
1. Edge Runtime 対応のテスト環境を使用する
まず最初にやったのは、Jest のテスト環境を Edge Runtime に近づけることです。具体的には、@edge-runtime/jest-environment
を testEnvironment
に設定しました。これを導入することで、Next.js 独自の Web API 実装と Node.js のモック環境との競合を減らすことができました。
// jest.config.js の例
{
"testEnvironment": "@edge-runtime/jest-environment"
}
これだけで、「is not a function
」エラーの発生頻度がかなり減りました。
2. 過剰なモックを減らす
テストをシンプルに保つことも重要でした。Edge Runtime 対応のテスト環境を使う場合、Response.json()
のモックは不要です。むしろ余計なモックを追加すると、新たな問題を引き起こす原因になりました。テスト対象の範囲を明確にし、必要最低限のモックだけを使うことで、問題を切り分けやすくなりました。
3. ビジネスロジックと Route Handler の分離
私が抱えていた最大の問題は、テスト対象の範囲が広すぎたことでした。Route Handler の中にビジネスロジックを直接埋め込んでしまっていたため、テストが複雑化していたのです。
そこで、ビジネスロジックを独立したクラスや関数に切り出しました。これにより、ビジネスロジックそのものは単体テストでカバーし、Route Handler 側ではそのロジックをモックするだけで済むようになりました。
4. モックの読み込み順序を明確化
Jest で ES Modules を使う際には、モジュールの読み込み順が非常に重要です。jest.mock()
を使う場合は、import
文よりも前に記述しなければモックが効きません。また、テスト間でモジュールキャッシュの影響を受けないよう、jest.resetModules()
や jest.isolateModules()
を活用しました。
5. パスエイリアスでモジュール管理を簡素化
プロジェクトの規模が大きくなると、相対パスが複雑になりがちです。そのため、tsconfig.json
や moduleNameMapper
を活用して、パスエイリアスを設定しました。これにより、モジュール間のパス管理がスムーズになり、テストコードの可読性も向上しました。
実際のコード例
具体例を挙げると、次のようにビジネスロジックと Route Handler のテストを分離しました。
ビジネスロジックの単体テスト
// line-login-service.test.ts
describe("LineLoginService", () => {
beforeEach(() => {
global.fetch = jest.fn();
});
it("APIのURLが設定されていない場合", async () => {
const service = new LineLoginService("");
const result = await service.getLoginUrl();
expect(result).toEqual({ error: "API URLが設定されていません" });
});
});
Route Handler のテスト(ビジネスロジックをモック)
// route.test.ts
jest.mock("@line-login-service", () => ({
LineLoginService: jest.fn().mockImplementation(() => ({
getLoginUrl: jest.fn().mockResolvedValue({ url: "https://line.login.url" }),
})),
}));
describe("LINE Login Route Handler", () => {
it("正常にURLを返す", async () => {
const response = await GET();
const body = await response.json();
expect(body).toEqual({ url: "https://line.login.url" });
});
});
最後に
今回の経験を通じて、「テストはシンプルであるべき」という基本的な考え方の大切さを改めて感じました。また、Edge Runtime のような特殊な環境でのテストは独特な課題が伴いますが、環境を整えるだけでも問題解決の糸口が見えてきます。
もし同じような問題に直面している方がいたら、この経験が参考になれば嬉しいです。そして、「まだこうした方が良いよ」というフィードバックがあれば、ぜひ教えてください!