👊エラーにぶち当たりながら学ぶJest環境構築(React×Vite×JS×TS)
まえがき
今いる開発現場が「テストをちゃんと書こうぜ」という感じ(いいことだね(´_ゝ`))なので、
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だけインストールしてみる。
npm create vite@latest vite-react-js-practice -- --template react
npm install jest --save-dev
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
+ "test": "jest"
},
既存のApp.jsx
を"HELLO WORLD"を返すだけのシンプルなものにして
const App = () => {
return <h1>HELLO WORLD</h1>;
};
export default App;
"HELLO WORLD"が表示されることを確認するテストを作成。
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
を作成する。
{
"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
{
"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
+ /**
+ * @jest-environment jsdom
+ */
import ReactDOM from "react-dom";
import App from "./App";
test("should render HELLO WORLD", () => {
それか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);
jsdom
からnode
に変更された。
✔ Jestは27系からtestEnvironmentのDefaultが詳細についてこちらの記事の説明が分かりやすかったので、引用させていただきます。
Jest@27からデフォルトのtestEnvironmentが変わり、26まではjsdomでしたが、27からnodeになりました。
この変更の背景には、
・nodeアプリ開発でテストを実行する際も、明記しなければjsdom環境で実行されてしまい、パフォーマンスが劣化
・しかもjsdom環境で実行されていることに気づけない
という問題があり、それらを救済するためにデフォルトをnodeに変更したようです。
この変更で、DOM APIを使うフロントエンドのテストではエラーが発生するようになりました。
エラーが発生するから気づけるよね! 自分で設定変えてね! ということらしいです。
import React from "react"
の記載がなくてもOKなようにする。
05. 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
を以下のように修正する。
{
"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だけインストールしてみる。
npm create vite@latest vite-react-ts-practice -- --template react-ts
npm install jest @types/jest --save-dev
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
+ "test": "jest"
},
既存のApp.tsx
を"HELLO WORLD"を返すだけのシンプルなものにして
const App = () => {
return <h1>HELLO WORLD</h1>;
};
export default App;
"HELLO WORLD"が表示されることを確認するテストを作成。
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
{
"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 | });
先ほどと同じ手順でtestEnvironment
をjsdom
に設定して、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。他にも色んなオプションをここで設定することが可能👍
module.exports = {
preset: "ts-jest",
globals: {
"ts-jest": {
tsconfig: "tsconfig.jest.json"
}
}
}
import React from "react"
の記載がなくてもOK。
✔ tsconfigオプション "jsx":"react-jsx"のおかげで、こちらの記事が参考になったので引用させていただく。
"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