🐥

👊エラーにぶち当たりながら学ぶJest環境構築(React×Vite×JS×TS)

2023/03/14に公開

まえがき

今いる開発現場が「テストをちゃんと書こうぜ」という感じ(いいことだね(´_ゝ`))なので、
Reactアプリ、NodeJSアプリのテストを書いたりレビューする機会が多い。

しかし、この記事をまとめるまではJest実行時にCommonJSのJSへ変換する必要があることさえもよく分かってなかったし(ほんと無知(´_ゝ`))、jest.config.jsらへんの中身も雰囲気で分かってるつもりだった(全然わかってなかった(´_ゝ`))。

ちゃんと理解するために、本稿では↓下記構成アプリのJest環境構築について、ぶちあたったエラーの原因分析&対処方法を残しながらまとめていく。

👉 Vite × React × JavaScript
👉 Vite × React × TypeScript

総括

✅Jestは実行時にテスト対象のソースコード(JS/TS)の変換を行ってくれるtransfomerが存在し、Defaultのtransformerとしてbabel-jest(Babelによる変換)が設定されている。

👉Jestの実行対象は「CommonJSで書かれたJS」であるため、ESModulesのimport/exportを使って書かれているコードはtransformer(babel-jest, ts-jest)で変換する必要がある。

👉Babel(babel-jest)で行う変換処理
✅JSX to JS変換 → @babel/preset-react
✅ESModules to CommonJS変換 → @babel/preset-env

✅Jestのテスト実行環境はtestEnvironmentオプションで設定可能。Defaultがnode(NodeJS環境)である。
👉documentを使うようなブラウザ実行想定のテスト(例:React)の場合、testEnvironmentをjsdomにする必要がある
✔ Jestは27系からtestEnvironmentのDefaultがjsdomからnodeに変更された。

✅ts-jestはtransfomerの1つ。TypeScript使ってるならこれ!
✅ts-jestは複数のpresetを保持している。presetを指定するだけでCommonJS構文へ変換してくれる👍

Vite × React × JavaScript

01. とりまJestだけインストールしてみる。

ViteのTemplateでプロジェクト構築
npm create vite@latest vite-react-js-practice -- --template react
Jestインストール(現時点で最新[29.5.0])
npm install jest --save-dev
package.json NPMスクリプト追加
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "preview": "vite preview",
+   "test": "jest"
  },

既存のApp.jsxを"HELLO WORLD"を返すだけのシンプルなものにして

App.jsx
const App = () => {
  return <h1>HELLO WORLD</h1>;
};

export default App;

"HELLO WORLD"が表示されることを確認するテストを作成。

App.test.jsx
import ReactDOM from "react-dom";
import App from "./App";

test("should render HELLO WORLD", () => {
  const el = document.createElement("div");
  ReactDOM.render(<App />, el);
  expect(el.innerHTML).toContain("HELLO WORLD");
});

これでnpm run testでテスト実行するとJSX読み取れんわと怒られる。

SyntaxError: C:\Users\daisu\Desktop\workspace\vite-react-js-practice\src\App.test.jsx: Support for the experimental syntax 'jsx' isn't currently enabled (6:19):

  4 | test("should render HELLO WORLD", () => {
  5 |   const el = document.createElement("div");
> 6 |   ReactDOM.render(<App />, el);
    |                   ^
  7 |   expect(el.innerHTML).toContain("HELLO WORLD");
  8 | });
  9 |

Add @babel/preset-react (https://github.com/babel/babel/tree/main/packages/babel-preset-react) to the 'presets' section of your Babel config to enable transformation.

👉エラーログの最後にある通り、@babel/preset-reactを使って、Jest実行時にJSX→JSへ変換する。

インストール
npm install babel @babel/preset-react --save-dev

BabelにJSX→JSへ変換してもらうよう、下記の.babelrcを作成する。

.babelrc
{
  "presets": ["@babel/preset-react"]
}

再度npm run test実行すると、↓別のエラーログが出てくるので、JSX→JS変換はOK👍

SyntaxError: Cannot use import statement outside a module

02. ん、Jest実行時に自動でbabel実行されるの?

✅Jestは実行時にコードの変換を行ってくれるtransfomerが存在し、Babelによる変換処理を行うbabel-jestがDefaultのtransformerに設定されている。
✅transformerはJestのtranformオプションで指定可能(指定なしだとbabel-jest)

※Transformerについての詳細 -> 公式

03. CommonJSで書かれたJSに変換する。

JSX→JS変換の件は解決し、次は「import文使えねえよ!」と怒られている。

SyntaxError: Cannot use import statement outside a module

👉Jestの実行対象は「CommonJSで書かれたJS」であるため、ESModulesのimport/exportを利用しているコードをテスト対象として実行できない。

なので、これまたbabelちゃんにESModules→CommonJSへの変換をお願いする。

ESModulesで書かれたJS(import/export文を使ったJS)
  ↓
CommonJSで書かれたJS(require/modules.export文を使ったJS)

✅@babel/preset-envに、ESModules→CommonJSの変換をしてもらう。

インストール
npm install @babel/preset-env --save-dev
.babelrc NodeJS上で実行できるCommonJSのJSに変換するよう設定する。
{
  "presets": [
    "@babel/preset-react",
+   ["@babel/preset-env", { "targets": { "node": "current" } }]
  ]
}

再度npm run test実行すると、↓別のエラーログが出てくるので、ESModules→CommonJS変換はOK👍

ReferenceError: document is not defined

      3 |
      4 | test("should render HELLO WORLD", () => {
    > 5 |   const el = document.createElement("div");
        |              ^
      6 |   ReactDOM.render(<App />, el);
      7 |   expect(el.innerHTML).toContain("HELLO WORLD");
      8 | });

04. テスト実行環境(testEnvironment)をjsdomに設定する。

ReferenceError: document is not defined

documentはブラウザ上で利用できるグローバルオブジェクトの1つである。そのブラウザ上で使えるはずのものが存在しないと怒られているので、テスト実行環境をブラウザ想定のものにする必要がある

✅Jestのテスト実行環境はtestEnvironmentオプションで設定可能。Defaultがnode(NodeJS環境)である。
👉documentを使うようなブラウザ実行想定のテストの場合、testEnvironmentをjsdomにする必要がある

インストール
npm install jest-environment-jsdom --save-dev
App.test.jsx 実行環境がjsdom上になるよう設定。
+ /**
+  * @jest-environment jsdom
+  */
import ReactDOM from "react-dom";
import App from "./App";

test("should render HELLO WORLD", () => {

それかjest.config.jsonに下記の行を追加すれば全テストに適用することが可能✅

jest.config.json
{
  "preset": "ts-jest",
+ "testEnvironment": "jest-environment-jsdom"
}

再度npm run test実行すると、↓別のエラーログが出てくるので、テスト実行環境問題はOK👍

ReferenceError: React is not defined

       7 | test("should render HELLO WORLD", () => {
       8 |   const el = document.createElement("div");
    >  9 |   ReactDOM.render(<App />, el);

✔ Jestは27系からtestEnvironmentのDefaultがjsdomからnodeに変更された。

詳細についてこちらの記事の説明が分かりやすかったので、引用させていただきます。

Jest@27からデフォルトのtestEnvironmentが変わり、26まではjsdomでしたが、27からnodeになりました。

この変更の背景には、
・nodeアプリ開発でテストを実行する際も、明記しなければjsdom環境で実行されてしまい、パフォーマンスが劣化
・しかもjsdom環境で実行されていることに気づけない
という問題があり、それらを救済するためにデフォルトをnodeに変更したようです。

この変更で、DOM APIを使うフロントエンドのテストではエラーが発生するようになりました。
エラーが発生するから気づけるよね! 自分で設定変えてね! ということらしいです。

05. import React from "react"の記載がなくてもOKなようにする。

ReferenceError: React is not defined

       7 | test("should render HELLO WORLD", () => {
       8 |   const el = document.createElement("div");
    >  9 |   ReactDOM.render(<App />, el);

こちらの記事にある通り、

👉React 17からimport React from "react"をコード内に含める必要がない。

なのに、書かなかったら"Reactがねえよ!"と怒られる。

参考記事にある手順にのっとり、.babelrcを以下のように修正する。

.babelrc
{
  "presets": [
+   ["@babel/preset-react", { "runtime": "automatic" }],
    ["@babel/preset-env", { "targets": { "node": "current" } }]
  ]
}

再度npm run test実行すると、問題なくテストがPASSされることを確認できた🎉

PASS  src/App.test.jsx
  √ should render HELLO WORLD (118 ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        3.143 s

Vite × React × TypeScript

01. とりまJestだけインストールしてみる。

ViteのTemplateでプロジェクト構築
npm create vite@latest vite-react-ts-practice -- --template react-ts
Jestインストール(現時点で最新[29.5.0])
npm install jest @types/jest --save-dev
package.json NPMスクリプト追加
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "preview": "vite preview",
+   "test": "jest"
  },

既存のApp.tsxを"HELLO WORLD"を返すだけのシンプルなものにして

App.tsx
const App = () => {
  return <h1>HELLO WORLD</h1>;
};

export default App;

"HELLO WORLD"が表示されることを確認するテストを作成。

App.test.tsx
import ReactDOM from "react-dom";
import App from "./App";

test("should render HELLO WORLD", () => {
  const el = document.createElement("div");
  ReactDOM.render(<App />, el);
  expect(el.innerHTML).toContain("HELLO WORLD");
});

これでnpm run testでテスト実行すると、先ほどと同じくJSX読み取れんわと怒られる。

SyntaxError: C:\Users\daisu\Desktop\workspace\vite-react-ts-practice\src\App.test.tsx: Support for the experimental syntax 'jsx' isn't currently enabled (6:19):

      4 | test("should render HELLO WORLD", () => {
      5 |   const el = document.createElement("div");
    > 6 |   ReactDOM.render(<App />, el);
        |                   ^
      7 |   expect(el.innerHTML).toContain("HELLO WORLD");
      8 | });
      9 |

Add @babel/preset-react (https://github.com/babel/babel/tree/main/packages/babel-preset-react) to the 'presets' section of your Babel config to enable transformation.

02. ts-jestをインストールする。

✅ts-jestはtransfomerの1つ。TypeScript使ってるならこれ!
✅ts-jestは複数のpresetを保持している。presetを指定するだけでCommonJS構文へ変換してくれる👍

インストール
npm install ts-jest --save-dev
jest.config.jsonを作成し、presetにts-jestを指定する。
{
  "preset": "ts-jest"
}

これでnpm run testでテスト実行すると下記のエラーで失敗するので、TS変換はOK👍

ReferenceError: document is not defined

      3 |
      4 | test("should render HELLO WORLD", () => {
    > 5 |   const el = document.createElement("div");
        |              ^
      6 |   ReactDOM.render(<App />, el);
      7 |   expect(el.innerHTML).toContain("HELLO WORLD");
      8 | });

先ほどと同じ手順でtestEnvironmentjsdomに設定して、npm run testを再実行するとテストが正常終了することを確認できた👍

PASS  src/App.test.tsx
  √ should render HELLO WORLD (71 ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        3.658 s
Ran all test suites.

✔globals.ts-jestでカスタム設定可能

Jest用のtsconfig.jsonを読み込ませたい場合は、↓のようにglobals.ts-jest配下に設定すればOK。他にも色んなオプションをここで設定することが可能👍

jest.config.js
module.exports = {
  preset: "ts-jest",
  globals: {
    "ts-jest": {
      tsconfig: "tsconfig.jest.json"
    }
  }
}

✔ tsconfigオプション "jsx":"react-jsx"のおかげで、import React from "react"の記載がなくてもOK。

こちらの記事が参考になったので引用させていただく。

"react" の場合はJSXが React.createElement に変換され、 "react-jsx" の場合は _jsx に変換される。それによりReactのimportが不要になるとのこと。

jest.config.js早見表

module.exports = {
  root: ["<rootDir>/src", "<rootDir>/tests"],
  preset: "ts-jest",
  testEnvironment: "node",
  transformIgnorePatterns: ["/node_modules/.*"],
  globals: {
    "ts-jest": {
      tsconfig: "tsconfig.jest.json"
    }
  },
  testRegex: ".+.test.tsx?$",
  moduleFileExtensions: ["ts", "tsx", "js"],
  collectCoverage: true,
  collectCoverageFrom: ["src/**/*", "server/**/*"],
  coveragePathIgnorePattern: ["/node_modules/", "src/types.d.ts"],
  reporters: ["default", "jest-junit"],
  resetMocks: false,
}
項目名 内容 Default
root
🔴preset ✅Jestのいろんな設定の詰め合わせ。
👉内部でtransformも設定されているので、presetを定義すればtransformの定義は不要。
✅TS変換時はts-jestを設定する。
undefined
🔵transform ✅Jest実行前に走るpreprocessor。
✅DEFAULTでbabel-jestが走る。
👉babel-jestの場合、<rootDir>/.babelrcを作成しないと何も変換処理は行われない。
{"\\.[jt]sx?$": "babel-jest"}
testEnvironment 基本DEFAULTの"node"でOK。jsdomいつ使う? node
transformIgnorePatterns Transformの対象外とするもの 省略
globals ✅テスト実行時に参照可能なグローバル変数を定義できる。
👉実際はts-jestで利用するtsconfig.jsonの指定によく使う。
省略
testRegex ✅テストファイルを見つける。
✅DEFAULTで.js/.jsx/.ts/.tsxが対象。 default it looks for .js, .jsx, .ts and .tsx files inside of tests folders,
moduleFileExtensions ✅テスト対象のファイル内でimport/exportにて拡張子省略可能かどうか。 省略
collectCoverage ✅カバレッジ取得する場合こいつをONにする。 false
collectCoverageFrom ✅カバレッジ取得対象のテストファイル。 undefined
coveragePathIgnorePattern ✅カバレッジ取得対象外とするテストファイル ["/node_modules/"]
reporters ✅Jest実行結果をレポートしてくれる人。
👉Defaultを上書く形でカスタマイズしたい場合は["default", "jest-junit"]となる。
default
resetMocks ✅Automatically reset mock state before every test. false

Discussion