🧪

Next.js (Edge Runtime) の Route Handler テストでつまずいた話と、その解決策

2025/01/28に公開

テストを書いたことのあるプログラマーなら一度は「なんでテストが通らないんだ…?」と頭を抱えた経験があると思います。今回、私もまさにその状態に陥りました。それは、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-environmenttestEnvironment に設定しました。これを導入することで、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.jsonmoduleNameMapper を活用して、パスエイリアスを設定しました。これにより、モジュール間のパス管理がスムーズになり、テストコードの可読性も向上しました。


実際のコード例

具体例を挙げると、次のようにビジネスロジックと 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 のような特殊な環境でのテストは独特な課題が伴いますが、環境を整えるだけでも問題解決の糸口が見えてきます。

もし同じような問題に直面している方がいたら、この経験が参考になれば嬉しいです。そして、「まだこうした方が良いよ」というフィードバックがあれば、ぜひ教えてください!

Discussion