Next.js + Jest のテスト環境を @swc/jest を使って高速化する
Next.js + jest のテスト環境を @swc/jest を使って高速化したので、このスクラップで、その環境構築方法を共有したいと思います💪
使用する技術
バージョンについて
"dependencies": {
"next": "11.0.1",
"react": "17.0.2",
"react-dom": "17.0.2"
},
"devDependencies": {
"@swc/core": "^1.2.85",
"@swc/jest": "^0.2.3",
"@testing-library/jest-dom": "^5.14.1",
"@testing-library/react": "^12.0.0",
"@types/jest": "^26.0.24",
"@types/react": "^17.0.14",
"jest": "^27.0.6",
"typescript": "^4.3.5"
}
参考記事
必要モジュールのインストール
# jestをインストール
$> yarn add -D jest @types/jest
# swcをインストール
$> yarn add -D @swc/core @swc/jest
# testing-libraryをインストール
$> yarn add -D @testing-library/react @testing-library/jest-dom
npm scriptsを設定
"scripts": {
"test": "jest",
},
Jestの設定
/**
* @type {import("@jest/types").Config.InitialOptions}
*/
const config = {
testEnvironment: "jsdom",
testMatch: ["**/*.test.js", "**/*.test.ts", "**/*.test.tsx"],
// @testing-library/react を使うためセットアップスクリプト( 後述 )
setupFilesAfterEnv: ["<rootDir>/src/test/utils/setup.ts"],
// 除外するフォルダーを指定する
testPathIgnorePatterns: [
"<rootDir>/src/test/utils/", // セットアップが入っているフォルダーは除外する
"<rootDir>/node_modules/",
"<rootDir>/.next/",
],
// コンパイル対象外のフォルダーを指定
transformIgnorePatterns: ["/node_modules/"],
transform: {
".+\\.(t|j)sx?$": [
"@swc/jest",
{
sourceMaps: true, // エラーを見やすくする( 有効じゃないと内容がズレて表示されます )
module: {
type: "commonjs", // 出力するファイルをcommonjsとする
},
jsc: {
parser: {
syntax: "typescript", // ソースコードをtypescriptとしてパースする
tsx: true, // jsx記法を許可する
},
transform: {
react: {
// 必須。省略すると "ReferenceError: React is not defined" が発生します
runtime: "automatic",
},
},
},
},
],
},
};
module.exports = config;
注意として、上記の設定は最低限動くような設定にしています。
もし他の設定が必要な場合は、各自 swc のドキュメントなどを見て適当に対応してください👇
setup.tsを実装する
import "@testing-library/jest-dom";
// Next.js特有の設定など
process.env = {
...process.env,
__NEXT_IMAGE_OPTS: {
deviceSizes: [320, 420, 768, 1024, 1200],
imageSizes: [],
domains: ["images.example.com"],
path: "/_next/image",
loader: "default",
} as any,
};
基本的に、import "@testing-library/jest-dom"
を実行するだけです。
Next.js特有の設定などが必要になる場合は、以下の記事などが参考になります👇
テストの実行
以下のように、yarn test
コマンドでテストが実行できていればOKです!
$> yarn test
yarn run v1.22.11
$ jest
PASS src/pages/index.test.tsx
トップページのテスト
✓ 'Welcome to Next.js!'が表示されているか (44 ms)
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 0.927 s, estimated 1 s
Ran all test suites.
Done in 1.57s.
おまけ
ts-jestで、上記と同じテストを実行した場合👇
$> yarn test
yarn run v1.22.11
$ jest
PASS src/pages/index.test.tsx
トップページのテスト
✓ 'Welcome to Next.js!'が表示されているか (45 ms)
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 4.373 s
Ran all test suites.
Done in 5.32s.
swc を使った時が 1.57s
なのに対し、ts-jest だと 5.32s
なので、大雑把に計算すると約5倍速くなっていますね。Wow 🤯
実行したテストスクリプト
上記で実行したテストの内容を以下に載せておきます👇
import { render, screen } from "@testing-library/react";
import TopPage from "./index.page"; // create-next-appで生成されたページコンポーネント
describe("トップページのテスト", () => {
test("'Welcome to Next.js!'が表示されているか", async () => {
render(<TopPage />);
const title = "Welcome to Next.js!";
expect(
screen.getByText((_, node) => {
if (!node) return false;
const nodeHasText = node.textContent === title;
const childrenDontHaveText = Array.from(node.children).every(
(child) => child.textContent !== title
);
return nodeHasText && childrenDontHaveText;
})
).toBeInTheDocument();
});
});
index.page.tsx
の実装内容は、create-next-app で生成されたモノとなっています。
また、pageExtensions
オプションによってページコンポーネントの拡張子を変更しています。詳しいことは、前に記事を書きましたので、そちらを参照して頂けると嬉しいです👇
baseUrl
によるエイリアスを有効にする
tsconfigのtsconfigのbaseUrl
を設定していると、その位置からのエイリアスを使用することができます。
例えば、src
フォルダーが以下のような構造だとします👇
.
├── src/
│ ├── hoge/
│ │ └── app.ts
│ └── hello/
│ └── world/
│ └── utils.ts
├── jest.config.js
└── tsconfig.json
この時、./src/hoge/app.ts
から ./src/hoge/utils.ts
をインポートしようとすると、
import anyModule from "../hello/world/utils"
と書けますが、baseUrl: './src'
と設定していると以下のように書けます🖊
import anyModule from "hello/world/utils" // srcからの相対位置で書ける
すごく便利なのですが、Jest で使用するには別途設定が必要になります👇
const fs = require("fs");
const tsconfig = require("./tsconfig.json");
// tsconfigから baseUrl を取得する
const baseUrl = tsconfig.compilerOptions.baseUrl;
// フォルダー名一覧を取得する
const folders = fs.readdirSync(baseUrl, { withFileTypes: true }).flatMap((ele) => {
return ele.isDirectory() ? [ele.name] : [];
});
/**
* @type {import("@jest/types").Config.InitialOptions}
*/
const config = {
// tsconfig.json の baseUrl によるエイリアスを有効にする
moduleNameMapper: folders.reduce((mapper, folderName) => {
return { ...mapper, [`^${folderName}(.*)$`]: `<rootDir>/src/${folderName}$1` };
}, {}),
/* -- 省略 -- */
};