Closed14

Next.js + Jest のテスト環境を @swc/jest を使って高速化する

uttkuttk

Next.js + jest のテスト環境を @swc/jest を使って高速化したので、このスクラップで、その環境構築方法を共有したいと思います💪

uttkuttk

使用する技術

バージョンについて

"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"
}
uttkuttk

必要モジュールのインストール

# 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を設定

package.json
  "scripts": {
    "test": "jest",
  },
uttkuttk

Jestの設定

./jest.config.js
/**
 * @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 のドキュメントなどを見て適当に対応してください👇

https://swc.rs/docs/configuring-swc

uttkuttk

setup.tsを実装する

./src/test/utils/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特有の設定などが必要になる場合は、以下の記事などが参考になります👇

https://zenn.dev/slowhand/articles/7bfe83207b434d

uttkuttk

テストの実行

以下のように、yarn testコマンドでテストが実行できていればOKです!

swcを使ってテストを実行
$> 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.
uttkuttk

おまけ

ts-jestで、上記と同じテストを実行した場合👇

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 🤯

uttkuttk

実行したテストスクリプト

上記で実行したテストの内容を以下に載せておきます👇

./src/pages/index.test.tsx
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 オプションによってページコンポーネントの拡張子を変更しています。詳しいことは、前に記事を書きましたので、そちらを参照して頂けると嬉しいです👇

https://zenn.dev/uttk/articles/d6be1c224494cb

uttkuttk

tsconfigのbaseUrlによるエイリアスを有効にする

tsconfigのbaseUrlを設定していると、その位置からのエイリアスを使用することができます。
例えば、srcフォルダーが以下のような構造だとします👇

.
├── src/
│   ├── hoge/
│   │   └── app.ts
│   └── hello/
│       └── world/
│           └── utils.ts
├── jest.config.js
└── tsconfig.json

この時、./src/hoge/app.ts から ./src/hoge/utils.ts をインポートしようとすると、

./src/hoge/app.ts
import anyModule from "../hello/world/utils"

と書けますが、baseUrl: './src' と設定していると以下のように書けます🖊

./src/hoge/app.ts
import anyModule from "hello/world/utils" // srcからの相対位置で書ける

すごく便利なのですが、Jest で使用するには別途設定が必要になります👇

./jest.config.js
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` };
  }, {}),

  /* -- 省略 -- */
};
このスクラップは2021/10/17にクローズされました