Next.js, Prisma, Jestでサーバーサイドのテストをする
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
サンプルプロジェクト
サンプルとして以下のようにインボイスを発行・取得するアプリケーションで考えていきます。
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>
</>
);
}
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;
};
// 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
上記のコマンドにより以下のファイルが生成されます。
/** @type {import('ts-jest').JestConfigWithTsJest} */
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
};
yarn test
でjestを実行できるようにしておきます。
...
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint",
"test": "jest" <- 追加
},
...
テストファイルを仮で用意します。
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
を読み込んでくれます。
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
でマイグレーションを実行できるようにしておきます。
"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のデータがテストケース間で共有されている状態です。
この状態でテストを実行すると、
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を使います。
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.ts
にtestEnvironment: "@quramy/jest-prisma-node/environment"
を設定します。
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を使っているため、
の通りに設定していきます。
...
// 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"],
}
...
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.
テストする
軽くテストを書いてみると、
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テストのフレームワークを活用するほかなさそうですか?
初めまして
React Testing Libraryは、Componentをレンダリングして操作するだけですので、Server Actionsをサポートしていないと思いますし、Next.jsではServer Actionsが発火するとHTTPリクエストが発生しますので、Jestの実行環境でHTTPリクエストを含めたテストをモックなしにテストするのは現状実装が難しいと思います。
回避策として
のどちらかを行うのが良いと思います。