🙌

Next.js, Prisma, Jestでサーバーサイドのテストをする

2023/12/25に公開2

App RouterやT3 Stackなどを使っていると、ビジネスロジックなどのサーバーサイドの処理も一つのNext.jsプロジェクトに含められるかと思います。
そういう場合のサーバーサイドの処理をJestでテストしてみます。

バージョン

Node.js: 20.9.0
Nexts.js: 14.0.4
jest: 29.7.0
ts-jest: 29.1.1

サンプルプロジェクト

https://github.com/hid3h/nextjs-jest-examples/tree/main/with-prisma

サンプルとして以下のようにインボイスを発行・取得するアプリケーションで考えていきます。

app/page.tsx
import {
  fetchLatestInvoice,
  issueInvoice,
} from "./services/invoice-serivce";

export default async function Home() {
  const createInvoice = async (formData: FormData) => {
    "use server";

    await issueInvoice({
      amount: Number(formData.get("amount")),
    });
  };

  const latestInvoice = await fetchLatestInvoice();

  return (
    <>
      <form action={createInvoice}>
        <input type="number" name="amount" />
        <button type="submit">Create Invoice</button>
      </form>
      <div>
        <h1>Latest Invoice</h1>
        <p>{latestInvoice?.amount}</p>
      </div>
    </>
  );
}
services/invoice-service.ts
import prisma from "../../db";

export const issueInvoice = async ({ amount }: { amount: number }) => {
  const invoice = await prisma.invoiceIssuance.create({
    data: {
      amount,
      issuedAt: new Date(),
    },
  });
  return invoice;
};

export const fetchLatestInvoice = async () => {
  const invoice = await prisma.invoiceIssuance.findFirst({
    orderBy: {
      issuedAt: "desc",
    },
  });
  return invoice;
};
db.ts
// https://www.prisma.io/docs/orm/more/help-and-troubleshooting/help-articles/nextjs-prisma-client-dev-practices

import { PrismaClient } from "@prisma/client";

const prismaClientSingleton = () => {
  return new PrismaClient();
};

declare global {
  var prisma: undefined | ReturnType<typeof prismaClientSingleton>;
}

const prisma = globalThis.prisma ?? prismaClientSingleton();

export default prisma;

if (process.env.NODE_ENV !== "production") globalThis.prisma = prisma;

Jestが動く環境を用意する

TypeScriptでテストを書いていきたいので、https://kulshekhar.github.io/ts-jest/docs/getting-started/installation に従ってts-jestを用意します。

yarn add -D jest ts-jest @types/jest
yarn ts-jest config:init

上記のコマンドにより以下のファイルが生成されます。

jest.config.js
/** @type {import('ts-jest').JestConfigWithTsJest} */
module.exports = {
  preset: 'ts-jest',
  testEnvironment: 'node',
};

yarn testでjestを実行できるようにしておきます。

packages.json
...
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "next lint",
    "test": "jest" <- 追加
  },
... 

テストファイルを仮で用意します。

__tests__/invoice.test.ts
test("adds 1 + 2 to equal 3", () => {
  expect(1 + 2).toBe(3);
});

これでひとまずjestが動く環境を作ることができました。

hide@hidenoMacBook-Pro with-prisma % yarn test              
yarn run v1.22.19
$ jest
 PASS  __tests__/invoice.test.ts
  ✓ adds 1 + 2 to equal 3 (1 ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        0.809 s, estimated 2 s
Ran all test suites.
✨  Done in 1.17s.

環境変数をセット

今のままでは環境変数は.envのものが使われてしまいます。
prismaのDATABASE_URLも開発環境のDBのものとなってしまいます。
今回は、test用のDBを用意して使いたいので、.env.testにtest用のDATABASE_URLを追加してテスト時に.env.testが読み込まれるようにします。

https://nextjs.org/docs/app/building-your-application/testing/jest のようにnext/jestを使うとテスト時に.env.testを読み込んでくれます。

jest.config.ts
import type { Config } from 'jest'
import nextJest from 'next/jest.js'

const createJestConfig = nextJest({
  // Provide the path to your Next.js app to load next.config.js and .env files in your test environment
  dir: './',
})

// Add any custom config to be passed to Jest
const config: Config = {
  preset: 'ts-jest',
  testEnvironment: 'node',
  // Add more setup options before each test is run
  // setupFilesAfterEnv: ['<rootDir>/jest.setup.ts'],
}

// createJestConfig is exported this way to ensure that next/jest can load the Next.js config which is async
export default createJestConfig(config)

ちなみに現状だとts-nodeが無いのでテストを実行すると以下のエラーが起きるかと思います。

Error: Jest: Failed to parse the TypeScript config file /Users/hide/hid3h/nextjs-jest-examples/with-prisma/jest.config.ts
  Error: Jest: 'ts-node' is required for the TypeScript configuration files. Make sure it is installed
Error: Cannot find package 'ts-node' imported from /Users/hide/hid3h/nextjs-jest-examples/with-prisma/node_modules/jest-config/build/readConfigFileAndSetRootDir.js
    at readConfigFileAndSetRootDir (/Users/hide/hid3h/nextjs-jest-examples/with-prisma/node_modules/jest-config/build/readConfigFileAndSetRootDir.js:116:13)
    at async readInitialOptions (/Users/hide/hid3h/nextjs-jest-examples/with-prisma/node_modules/jest-config/build/index.js:403:13)
    at async readConfig (/Users/hide/hid3h/nextjs-jest-examples/with-prisma/node_modules/jest-config/build/index.js:147:48)
    at async readConfigs (/Users/hide/hid3h/nextjs-jest-examples/with-prisma/node_modules/jest-config/build/index.js:424:26)
    at async runCLI (/Users/hide/hid3h/nextjs-jest-examples/with-prisma/node_modules/@jest/core/build/cli/index.js:151:59)
    at async Object.run (/Users/hide/hid3h/nextjs-jest-examples/with-prisma/node_modules/jest-cli/build/run.js:130:37)
error Command failed with exit code 1.
info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.

ts-nodeをインストールしておきます。

yarn add -D ts-node

これでJest実行時に環境変数として.env.testのものが読み込まれるようになりました。

テスト用のDBのマイグレーション

yarn prisma:migrate:test

でマイグレーションを実行できるようにしておきます。

package.json
 "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "next lint",
    "test": "jest",
    "prisma:migrate:test": "DATABASE_URL=postgresql://johndoe:randompassword@localhost:5432/mydb-test?schema=public prisma migrate dev"
  },

(DATABASE_URLの値がここと、.env.testと2つに定義する必要があるので、もっといい方法ないかなと)

テストケースごとにDBのデータをリセットする

現状だと、DBのデータがテストケース間で共有されている状態です。

この状態でテストを実行すると、

__tests__/invocei.test.ts
import { issueInvoice } from "../app/services/invoice-serivce";
import prisma from "../db";

test("請求書発行", async () => {
  await issueInvoice({ amount: 1000 });
});

test("最新の請求書取得", async () => {
  console.log(await prisma.invoiceIssuance.findMany())!;
});

(2回動作させたので2レコード生成されています)

hide@hidenoMacBook-Pro with-prisma % yarn test
yarn run v1.22.19
$ jest
  console.log
    [
      {
        id: 'clqhsqin20000uvbwfb5sn55x',
        amount: 1000,
        issuedAt: 2023-12-23T08:26:18.151Z
      },
      {
        id: 'clqhsr28m0000mttwd8uls4yi',
        amount: 1000,
        issuedAt: 2023-12-23T08:26:43.567Z
      }
    ]

      at Object.<anonymous> (__tests__/invoice.test.ts:9:11)

 PASS  __tests__/invoice.test.ts
  ✓ 請求書発行 (52 ms)
  ✓ 最新の請求書取得 (10 ms)

Test Suites: 1 passed, 1 total
Tests:       2 passed, 2 total
Snapshots:   0 total
Time:        0.161 s, estimated 1 s
Ran all test suites.
✨  Done in 1.45s.

DBにデータが残ったままになっています。

(mockしてDBにレコードを生成しないテストのやり方もあるかと思いますが、今回はDBにデータを生成する方法でやります)

jest-prisma-nodeを使います。
https://github.com/Quramy/jest-prisma/tree/main/packages/jest-prisma-node
jest-prismaだとテストを実行したときに以下のエラーが出るため、jest-prisma-nodeを使っています。

hide@hidenoMacBook-Pro with-prisma % yarn test                   
yarn run v1.22.19
$ jest
 FAIL  __tests__/invoice.test.ts
  ● Test suite failed to run

    Cannot find module 'jest-environment-jsdom'
    Require stack:
    - /Users/hide/hid3h/nextjs-jest-examples/with-prisma/node_modules/@quramy/jest-prisma/lib/environment.js
    - /Users/hide/hid3h/nextjs-jest-examples/with-prisma/node_modules/jest-util/build/requireOrImportModule.js
    - /Users/hide/hid3h/nextjs-jest-examples/with-prisma/node_modules/jest-util/build/index.js
    - /Users/hide/hid3h/nextjs-jest-examples/with-prisma/node_modules/@jest/core/build/FailedTestsInteractiveMode.js
    - /Users/hide/hid3h/nextjs-jest-examples/with-prisma/node_modules/@jest/core/build/plugins/FailedTestsInteractive.js
    - /Users/hide/hid3h/nextjs-jest-examples/with-prisma/node_modules/@jest/core/build/watch.js
    - /Users/hide/hid3h/nextjs-jest-examples/with-prisma/node_modules/@jest/core/build/cli/index.js
    - /Users/hide/hid3h/nextjs-jest-examples/with-prisma/node_modules/@jest/core/build/index.js
    - /Users/hide/hid3h/nextjs-jest-examples/with-prisma/node_modules/jest-cli/build/run.js
    - /Users/hide/hid3h/nextjs-jest-examples/with-prisma/node_modules/jest-cli/build/index.js
    - /Users/hide/hid3h/nextjs-jest-examples/with-prisma/node_modules/jest-cli/bin/jest.js
    - /Users/hide/hid3h/nextjs-jest-examples/with-prisma/node_modules/jest/bin/jest.js

      at Function.call (node_modules/@cspotcode/source-map-support/source-map-support.js:811:30)

Test Suites: 1 failed, 1 total
Tests:       0 total
Snapshots:   0 total
Time:        0.026 s, estimated 1 s
Ran all test suites.
error Command failed with exit code 1.
info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.

jest-prisma-nodeをインストールします。

yarn add -D @quramy/jest-prisma-node

jest.config.tstestEnvironment: "@quramy/jest-prisma-node/environment"を設定します。

jest.config.ts
import type { Config } from 'jest'
import nextJest from 'next/jest.js'

const createJestConfig = nextJest({
  // Provide the path to your Next.js app to load next.config.js and .env files in your test environment
  dir: './',
})

// Add any custom config to be passed to Jest
const config: Config = {
  preset: 'ts-jest',
  testEnvironment: "@quramy/jest-prisma-node/environment",
  // Add more setup options before each test is run
  // setupFilesAfterEnv: ['<rootDir>/jest.setup.ts'],
}

// createJestConfig is exported this way to ensure that next/jest can load the Next.js config which is async
export default createJestConfig(config)

そのあとは、
シングルトンのPrismaClientを使っているため、
https://github.com/Quramy/jest-prisma?tab=readme-ov-file#singleton
の通りに設定していきます。

jest.config.ts
...
// Add any custom config to be passed to Jest
const config: Config = {
  preset: 'ts-jest',
  testEnvironment: "@quramy/jest-prisma/environment",
  // Add more setup options before each test is run
  setupFilesAfterEnv: ["<rootDir>/setup-prisma.js"],
}
...
setup-prisma.js
jest.mock("./db", () => {
  return jestPrisma.client;
});

db.ts
export default prisma;
としてるので

return {
 prisma: jestPrisma.client,
};

ではなく、

return jestPrisma.client;

としています。

これで、一つのテストが終わるとDBのデータをリセットできるようになりました。

hide@hidenoMacBook-Pro with-prisma % yarn test                     
yarn run v1.22.19
$ jest
  console.log
    []

      at Object.<anonymous> (__tests__/invoice.test.ts:9:11)

 PASS  __tests__/invoice.test.ts
  ✓ 請求書発行 (245 ms)
  ✓ 最新の請求書取得 (38 ms)

Test Suites: 1 passed, 1 total
Tests:       2 passed, 2 total
Snapshots:   0 total
Time:        0.365 s, estimated 1 s
Ran all test suites.
✨  Done in 1.66s.

テストする

軽くテストを書いてみると、

__tests__/invoice.test.ts
import {
  fetchLatestInvoice,
  issueInvoice,
} from "../app/services/invoice-serivce";
import prisma from "../db";

test("請求書発行", async () => {
  await issueInvoice({ amount: 1000 });

  const invoiceIssuance = await prisma.invoiceIssuance.findFirst();
  expect(invoiceIssuance?.amount).toBe(1000);
});

test("最新の請求書取得", async () => {
  await issueInvoice({ amount: 1000 });
  await issueInvoice({ amount: 2000 });

  const latestInvoiceIssuance = await fetchLatestInvoice();
  expect(latestInvoiceIssuance?.amount).toBe(2000);
});

実行すると

hide@hidenoMacBook-Pro with-prisma % yarn test
yarn run v1.22.19
$ jest
 PASS  __tests__/invoice.test.ts
  ✓ 請求書発行 (71 ms)
  ✓ 最新の請求書取得 (39 ms)

Test Suites: 1 passed, 1 total
Tests:       2 passed, 2 total
Snapshots:   0 total
Time:        0.201 s, estimated 1 s
Ran all test suites.
✨  Done in 1.53s.

となり正常に動作することが確認できました。

Discussion

ぴんなるぴんなる

参考になります、ありがとうございます!
Testing-Libraryを使用して、フロントエンドの操作して(formをrenderして、clickしてsubmitさせる)server actionを動かそうと思ったのですが、server actiomが動きません(エラーもでず)。。
そういったテストはe2eテストのフレームワークを活用するほかなさそうですか?

Junsei NagaoJunsei Nagao

初めまして

React Testing Libraryは、Componentをレンダリングして操作するだけですので、Server Actionsをサポートしていないと思いますし、Next.jsではServer Actionsが発火するとHTTPリクエストが発生しますので、Jestの実行環境でHTTPリクエストを含めたテストをモックなしにテストするのは現状実装が難しいと思います。

回避策として

  • Server Actionsで実行しているロジックを切り出して関数単体でテストする(入力はテストケースで値を渡す、依存はモックする)
  • おっしゃるようにE2EテストでNext.jsサーバーを立ち上げたうえでテストする

のどちらかを行うのが良いと思います。