Next.js にて Jest を導入する

導入から簡単なテストの実施まで。
基本的に下記内容をなぞる。

環境

  • Next.js v15
  • React v19 → v18
  • TypeScript v5
  • App Router

Quickstart

$ npx create-next-app@latest --example with-jest with-jest-app

Manual setup

React のバージョンが19以上だとインストール時にエラーが起きるので、
バージョンを18にする必要があった。
--force をつけて強制的にインストールすることもできるけど、
中々に危ないので、バージョンを落とす。

# エラーが出る。
$ npm install --save-dev jest jest-environment-jsdom @testing-library/react @testing-library/dom @testing-library/jest-dom ts-node

Could not resolve dependency:
peer react@"^18.0.0 || ^19.0.0" from @testing-library/react@16.2.0

Fix the upstream dependency conflict, or retry
this command with --force or --legacy-peer-deps
to accept an incorrect (and potentially broken) dependency resolution.

# Next.js と React をアンインストールする
$ npm uninstall --save next react react-dom

# バージョンを指定してインストールする
$ npm install --save next@latest react@18 react-dom@18

セットアップ

自分が選択した内容。適当。

  • yes
  • yes
  • jsdom
  • yes
  • v8
  • no
$ npm init jest@latest

✔ Would you like to use Jest when running "test" script in "package.json"? … yes
✔ Would you like to use Typescript for the configuration file? … yes
Choose the test environment that will be used for testing › jsdom (browser-like)
✔ Do you want Jest to add coverage reports? … yes
✔ Which provider should be used to instrument code for coverage? › v8
✔ Automatically clear mock calls, instances, contexts and results before every test? … no

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: "./",
})

const config: Config = {

	// Indicates whether the coverage information should be collected while executing the test
	collectCoverage: true,

	// The directory where Jest should output its coverage files
	coverageDirectory: "coverage",

	// Indicates which provider should be used to instrument code for coverage
	coverageProvider: "v8",

	// A list of reporter names that Jest uses when writing coverage reports
	coverageReporters: [
		// "json",
		"text",
		"lcov",
		// "clover"
	],

	// A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module
	moduleNameMapper: {
		"^@/(.*)$": "<rootDir>/src/$1",
	},

	// A list of paths to modules that run some code to configure or set up the testing framework before each test
	setupFilesAfterEnv: ['<rootDir>/jest.setup.ts'],

	// The test environment that will be used for testing
	// testEnvironment: "jsdom",
	testEnvironment: "./CustomJSDOMEnvironment.ts",
}

export default createJestConfig(config)

jest.setup.ts

jest.config.ts の setupFilesAfterEnv で必要。

import "@testing-library/jest-dom"

CustomJSDOMEnvironment.ts

jest.config.ts の testEnvironment で必要。

import JSDOMEnvironment from "jest-environment-jsdom"

// https://qiita.com/takano-h/items/506fa48493873bf7af41
// https://github.com/jsdom/jsdom/issues/1724#issuecomment-1446858041
// https://github.com/facebook/jest/blob/v29.4.3/website/versioned_docs/version-29.4/Configuration.md#testenvironment-string
export default class CustomJSDOMEnvironment extends JSDOMEnvironment {
	constructor(...args: ConstructorParameters<typeof JSDOMEnvironment>) {
		super(...args)

		// https://github.com/mswjs/jest-fixed-jsdom/blob/main/index.js
		// this.customExportConditions = args.customExportConditions || [""]

		this.global.TextDecoder = TextDecoder
		this.global.TextEncoder = TextEncoder
		this.global.TextDecoderStream = TextDecoderStream
		this.global.TextEncoderStream = TextEncoderStream
		this.global.ReadableStream = ReadableStream

		// FIXME https://github.com/jsdom/jsdom/issues/1724
		if (!this.global.fetch) {
			this.global.fetch = fetch
			this.global.Headers = Headers
			this.global.Request = Request
			this.global.Response = Response
		}
	}
}

tsconfig.json

コード抜粋。
jest.config.ts の moduleNameMapper で必要。

{
	"compilerOptions": {
		"paths": {
			"@/*": [
				"./src/*"
			]
		}
	}
}

package.json

コード抜粋。

{
	"scripts": {
		"test": "jest"
	}
}

簡単なテストを実施する

テストファイルの置き場所は宗教があるみたい。
一旦内容をなぞる形で進める。

/src/__tests__/test.test.ts

import { describe, expect, test } from "@jest/globals"

function sum(a, b) {
	return a + b
}

describe("sum() のテスト", () => {
	test("1 + 2 は 3 です", () => {
		expect(sum(1, 2)).toBe(3)
	})
})

テスト開始

$ npm run test
> sample@0.1.0 test
> jest

(node:17328) [DEP0040] DeprecationWarning: The `punycode` module is deprecated. Please use a userland alternative instead.
(Use `node --trace-deprecation ...` to show where the warning was created)
 PASS  src/__tests__/test.test.ts
  sum() のテスト
    ✓ 1 + 2 は 3 です (1 ms)

----------|---------|----------|---------|---------|-------------------
File      | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
----------|---------|----------|---------|---------|-------------------
All files |       0 |        0 |       0 |       0 |
----------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        0.534 s, estimated 1 s
Ran all test suites.