株式会社HAMWORKS
📝

Next.js + NextAuth.js(現Auth.js) を使った環境において API Routes のテストをするまでの道のり

2024/03/07に公開

はじめに

Next.js + NextAuth.js(現Auth.js) を使った環境において next-test-api-route-handler を使って API Routes のテストをするまでの道のりを書きました。

私は既存の環境で next-test-api-route-handler を動かしたいだけなのに、色々とエラーで躓いたので、もしも私と似たような環境下に置かれた人の助けになれば幸いです。

※もろもろ環境がちょっと古いのはご容赦いただければと思います。

対象読者

この記事は以下のような人を対象としています。

  • Next.js を使っている人
    • Pages Router を利用している人
  • NextAuth.js(現Auth.js) を使った環境において next-test-api-route-handler を利用押してテストをしたい人
  • msw を v1 => v2 にマイグレーションしたい人

環境

  • Node v18.17.0
  • npm v9.6.7
  • Next.js v13.4.12
  • NextAuth.js v4.22.4
  • Jest v29.7.0
  • Jest Environment JSDOM v29.7.0

next-test-api-route-handler v3 系のインストール

next-test-api-route-handler の v4 系が最近リリースされましたが、情報が多い v3系を使います。

npm i -D next-test-api-route-handler@v3 

https://www.npmjs.com/package/next-test-api-route-handler/v/3.2.0

エラーが発生

すでにプロジェクトに導入済みだった msw v1系が駄目だったもようです。
v2系にアップデートしろと言われました。

npm ERR! code ERESOLVE
npm ERR! ERESOLVE could not resolve
npm ERR! 
npm ERR! While resolving: next-test-api-route-handler@3.2.0
npm ERR! Found: msw@1.2.3
npm ERR! node_modules/msw
npm ERR!   dev msw@"^1.2.3" from the root project
npm ERR! 
npm ERR! Could not resolve dependency:
npm ERR! peerOptional msw@">=2" from next-test-api-route-handler@3.2.0
npm ERR! node_modules/next-test-api-route-handler
npm ERR!   dev next-test-api-route-handler@"v3" from the root project
npm ERR! 
npm ERR! Conflicting peer dependency: msw@2.2.2
npm ERR! node_modules/msw
npm ERR!   peerOptional msw@">=2" from next-test-api-route-handler@3.2.0
npm ERR!   node_modules/next-test-api-route-handler
npm ERR!     dev next-test-api-route-handler@"v3" from the root project

msw v2 も併せてインストールする

https://www.npmjs.com/package/msw/v/2.2.2

npm i -D next-test-api-route-handler@v3 msw@v2

今回はエラーが発生せず、無事にインストールが完了しました。

msw v1 to v2

msw v2になったら import 先やらレスポンスの返し方が変わったようなので、マイグレーションを行います。

ここでは自分が書き換えた部分のみ掲載します。
詳しいマイグレーション手順は msw の公式を参照してください。

https://mswjs.io/docs/migrations/1.x-to-2.x/

Worker imports

Before

import { setupWorker } from "msw";

After

import { setupWorker } from "msw/browser";

レスポンスの返却

Before

import { rest } from "msw";

export const handlers = [
  rest.get("/hello-world", (req, res, ctx) => {
    return res(
      ctx.json({ message: "Hello World" })
    )
  }),
];

After

import { http, HttpResponse } from "msw";

export const handlers = [
  http.get("/hello-world", () => {
    return HttpResponse.json({ message: "Hello World" });
  }),
];

Jest の設定を見直す

前述のマイグレーションを済ませたうえで、Jestで対象のテストを走らせると次のようなエラーが発生してテストが失敗しました。

エラーその1:Cannot find module 'msw/node'

  ● Test suite failed to run

    Cannot find module 'msw/node' from 'src/mocks/server.ts'

msw/node が見つからないと言われています。

以下は公式からの引用です。

This error is thrown by your test runner because JSDOM uses the browser export condition by default. This means that when you import any third-party packages, like MSW, JSDOM forces its browser export to be used as the entrypoint. This is incorrect and dangerous because JSDOM still runs in Node.js and cannot guarantee full browser compatibility by design.

JSDOMの export が原因みたいです。
msw 公式に対応方法が記載されていたので、以下を jest.config.js に適用します。

jest.config.js

jest.config.js
module.exports = {
  // 略
  testEnvironmentOptions: {
    customExportConditions: [''],
  },
}

エラーその2:ReferenceError: TextEncoder is not defined

前述の設定を jest.config.js に適用したうえで、再度テストを走らせます。

すると今度は次のエラーが発生しました。

  ● Test suite failed to run

    ReferenceError: TextEncoder is not defined

TextEncoder が未定義だと言われました。
コレに関してもすでに msw 公式に対応方法の記載がありました。
jest.polyfills.js というファイルを作って、jest.config.js に適用します。

jest.polyfills.js

jest.polyfills.js
const { TextDecoder, TextEncoder } = require('node:util');
 
Object.defineProperties(globalThis, {
  TextDecoder: { value: TextDecoder },
  TextEncoder: { value: TextEncoder },
});
 
const { Blob, File } = require('node:buffer');
const { fetch, Headers, FormData, Request, Response } = require('undici');
 
Object.defineProperties(globalThis, {
  fetch: { value: fetch, writable: true },
  Blob: { value: Blob },
  File: { value: File },
  Headers: { value: Headers },
  FormData: { value: FormData },
  Request: { value: Request },
  Response: { value: Response },
});

jest.config.js

jest.config.js
module.exports = {
  // 略
+  setupFiles: ['./jest.polyfills.js'],
  testEnvironmentOptions: {
    customExportConditions: [''],
  },
}

エラーその3:Cannot find module 'undici' from 'jest.polyfills.js'

jest.polyfills.js というファイルを作って、jest.config.js に適用したうえで、再度テストを走らせます。

すると今度は下記のエラーが発生しました。

  ● Test suite failed to run

    Cannot find module 'undici' from 'jest.polyfills.js'

      19 | const { Blob, File } = require("node:buffer");
      20 | const { fetch, Headers, FormData, Request, Response } = require("undici");
    > 21 |
         | ^
      22 | Object.defineProperties(globalThis, {
      23 |   fetch: { value: fetch, writable: true },
      24 |   Blob: { value: Blob },

undici というpackageをインストールする必要があるみたいです。

npm i -D undici@v6

https://undici.nodejs.org/#/

エラーその4:ReferenceError: ReadableStream is not defined

再度テストを走らせると ReadableStream が未定義だと言われました。
先程の jest.polyfills.js に下記を追加します。

jest.polyfills.js

jest.polyfills.js
const { TextDecoder, TextEncoder } = require('node:util');
 
Object.defineProperties(globalThis, {
  TextDecoder: { value: TextDecoder },
  TextEncoder: { value: TextEncoder },
});

+ const { ReadableStream } = require('node:stream/web');
+ Object.defineProperties(globalThis, {
+   ReadableStream: { value: ReadableStream },
+ });
 
const { Blob, File } = require('node:buffer');
const { fetch, Headers, FormData, Request, Response } = require('undici');
 
Object.defineProperties(globalThis, {
  fetch: { value: fetch, writable: true },
  Blob: { value: Blob },
  File: { value: File },
  Headers: { value: Headers },
  FormData: { value: FormData },
  Request: { value: Request },
  Response: { value: Response },
});

この状態で再度テストを走らせると、 msw を利用しているテストは成功するようになることでしょう。
※成功しなかった人はごめんなさい...

参考記事

https://zenn.dev/krs1/articles/85c91ed2846fd1

next-test-api-route-handler を使ったAPI Routesのテストを行う

ここからが本題です。

まずは雑な API Routes で試してみましょう。

APIとテスト用ファイルを作成する

下記のような API Routes を作成します。

src/pages/api/v1/hello-world.ts
import type { NextApiRequest, NextApiResponse } from 'next'

type ResponseData = {
  message: string
}

export default function handler(
  req: NextApiRequest,
  res: NextApiResponse<ResponseData>
) {
  res.status(200).json({ message: 'Hello from Next.js!' })
}

次にテストファイルを作成します。

next-src/tests/api/v1/hello-world.test.ts
import { testApiHandler } from "next-test-api-route-handler";

import handler from "@pages/api/v1/hello-world";

describe("API Hello World", () => {
  test("GET", async () => {
    await testApiHandler({
      handler,
      url: "/api/v1/hello-world",
      test: async ({ fetch }) => {
        const res = await fetch();
        const data = await res.json();
        const { status } = res;
        expect(status).toBe(200);
        expect(data).toEqual({ message: "Hello from Next.js!" });
      },
    });
  });
});

テストを実行すると問題無く成功することでしょう。

NextAuth.js を介すAPIの場合

NextAuth.js に依存するAPIのテスト行うと下記のようなエラーが発生しました。

getServerSession を使っているようなAPIを指します

● Test suite failed to run

    Jest encountered an unexpected token

    Jest failed to parse a file. This happens e.g. when your code or its dependencies use non-standard JavaScript syntax, or when Jest is not configured to support such syntax.

    Out of the box Jest supports Babel, which will be used to transform your files into valid JS based on your Babel configuration.

    By default "node_modules" folder is ignored by transformers.
SyntaxError: Unexpected token 'export'

https://zenn.dev/sjbworks/articles/452d20632ffbc1

こちらの記事を参考に jest.config.js を修正しました。

jest.config.js
const nextJest = require('next/jest')

const createJestConfig = nextJest({
  dir: './',
})

const customJestConfig = {
  //  略
  setupFiles: ['./jest.polyfills.js'],
  testEnvironmentOptions: {
    customExportConditions: [''],
  }
};

- module.exports = createJestConfig(customJestConfig);
+ module.exports = async () => {
+   return {
+     ...(await createJestConfig(customJestConfig)()),
+     transformIgnorePatterns: ["node_modules/(?!(jose|next-auth|@panva|uuid)/)"],
+   }
+ };

transformIgnorePatterns に NextAuth.js が依存するパッケージを設定しました。
その後、テストが動作するようになりました。

お疲れ様でした!!

おわりに

本題の方が短くなってしまいました。

ただ、記事執筆時点では getServerSession のセッション情報が必要なAPIのテスト方法がわかっていない状況です。

そもそも認証情報が必要なテストは、単体テストで行うのかまたはE2Eや結合テストで行うのか、その辺りの知見が無いです。

方法がわかりしだい記事にしたいと思います。

株式会社HAMWORKS
株式会社HAMWORKS

Discussion