この手順でやればReact + Webpack環境にテストを導入できるはずという記事。
Dependencies
JavaScript ファイルをテストするためにjest
、
TypeScript ファイルをテストするためにts-jest
、
React ファイルをテストするためにRTL
jest
ts-jest
babel-jest
@babel/core
@babel/preset-env
@testing-library/react
@testing-library/jest-dom
@testing-library/user-event
jest-environment-jsdom
@types/jest
@types/testing-library__react
@types/testing-library__user-event
@types/testing-library__jest-dom
手順
1. jest と ts-jest の設定
目標: ECMAScript 文法、且つ TypeScript で書かれた JavaScript ファイルのテストを可能とさせる。
以下のような設定を行っていく:
- CommonJS 文法で書かれたファイルのテストは babel-jest が行う
- TypeScript で書かれたファイルのテストは ts-jest が行う
- global 型情報の取得
- ECMAScript 文法を許容させる
- test ファイル群は
<rootDir>/__tests__/
へ納める - ビルド時に
src/__tests__
などテスト関係が含まれないように webpack の設定を変更する - テスト用の tsconfig ファイルを用意する
Installation:
# jestのインストール
$ yarn add --dev jest
# テストのためのbabelのインストール
$ yarn add --dev babel-jest @babel/core @babel/preset-env
# ts-jestのインストール
$ yarn add --dev ts-jest
# 型情報のインストール
$ yarn add --dev @types/jest
コンフィグファイルの構成:
# jest.config.jsが出力される
npx ts-jest config:init
# <rootDir>/jest.config.js
以下のような内容になっている
// jest.config.js
/** @type {import('ts-jest').JestConfigWithTsJest} */
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
};
jest に TypeScript 拡張子ファイルに対しては ts-jest を使うように認識させる
transform
プロパティを定義することで設定できる。
transform
プロパティは Node がサポートしていない JavaScript 構文をサポート可能な構文に変換するための設定を定義するところである。
デフォルトだと.ts
, .js
, .tsx
, .jsx
は babel-jest が変換処理を行うことになっている。
これにts-jest
の設定を追加することで TypeScript ファイルをテスト可能とさせる。
ts-jest
の設定でtransform
設定を定義する場合はpreset
設定を除外すること。
If you are using custom transform config, please remove preset from your Jest config to avoid issues that Jest doesn't transform files correctly.
// jest.config.js
/** @type {import('ts-jest').JestConfigWithTsJest} */
module.exports = {
- preset: 'ts-jest',
testEnvironment: 'node',
+ transform: {
+ // '^.+\\.[tj]sx?$' to process js/ts with `ts-jest`
+ // '^.+\\.m?[tj]sx?$' to process js/ts/mjs/mts with `ts-jest`
+ '^.+\\.(ts|tsx)?$': [
+ 'ts-jest',
+ {
+ // define ts-jest settings
+ },
+ ]
+ },
};
jest に TypeScript 拡張子ファイル以外に対しては bable-jest を使うように認識させる
TypeScript 拡張子ファイルを ts-jest に変換させるルールを書いたら、
デフォルトの{"\\.[jt]sx?$": "babel-jest"}
設定がなくなっているので、
TypeScript 拡張子以外の JavaScript ファイルはお前がやってくれと設定を追加しなくてはならない。
Remember to include the default babel-jest transformer explicitly, if you wish to use it alongside with additional code preprocessors:
// jest.config.js
/** @type {import('ts-jest').JestConfigWithTsJest} */
module.exports = {
- preset: 'ts-jest',
testEnvironment: 'node',
transform: {
'^.+\\.(ts|tsx)?$': [
'ts-jest',
{
// define ts-jest settings
},
],
+ '^.+\\.(js|jsx)$': 'babel-jest',
},
};
babel のコンフィグを定義する
# on root directory
$ touch .babelrc.json
.bablerc.json
:
{
"presets": [["@babel/preset-env", {"targets": {"node": "current"}}]],
}
current
の部分は使用環境の Node のバージョンを指定する。
.babelrc
とbabel.config.js
どちらを定義すればいいのかについて
ECMAScript 文法を許容させる
jest は ECMAScript 文法のサポートは実験的なサポートしか提供していない、とのこと。
// jest.config.js
/** @type {import('ts-jest').JestConfigWithTsJest} */
module.exports = {
testEnvironment: 'node',
+ extensionsToTreatAsEsm: ['.ts', '.tsx', '.jsx'],
transform: {
'^.+\\.(ts|tsx)?$': [
'ts-jest',
{
+ useESM: true
},
],
'^.+\\.(js|jsx)$': 'babel-jest',
},
};
-
ts-jest
はuseESM
を有効にする -
extensionsToTreatAsESM
に ESM 文法を使う拡張子を指定する。
jest は package.json で
"type": "module"
が定義されているときに.js
や.mjs
のファイルを ECMAScript として扱う。
.js
は常に package.json の設定に従って常に(ESM だと)推測されるから含めるなというエラーが発生するので.js
は含めない。
● Validation Error:
Option: extensionsToTreatAsEsm: ['.ts', '.tsx', '.jsx', '.js'] includes '.js' which is always inferred based on type in its nearest package.json.
import 文なしでテスト API を使えるようにする
テストファイルでいちいちimport <テストAPI>
したくないための設定。
API を使用するには
-
@jest/globals
をインストールして、各 test ファイルは import 文で取得する -
@types/jest
をインストールする
@types/jest
をインストールする方法を採用した。
インストールするだけだとまったく認識してくれない。
次の通りに設定する。
tsconfig.jest.json
:
{
"compilerOptions": {
+ types: ["jest"]
}
}
test ファイル群のディレクトリを認識させる
// jest.config.js
/** @type {import('ts-jest').JestConfigWithTsJest} */
module.exports = {
testEnvironment: 'node',
extensionsToTreatAsEsm: ['.ts', '.tsx', '.jsx'],
transform: {
'^.+\\.(ts|tsx)?$': [
'ts-jest',
{
useESM: true
},
],
'^.+\\.(js|jsx)$': 'babel-jest',
},
+ testMatch: [
+ '**/__tests__/**/*.+(ts|tsx|js)',
+ '**/?(*.)+(spec|test).+(ts|tsx|js)',
+ '!**/__tests__/setup-jest.js',
+ ],
};
test ファイル群を指定するプロパティはtestMatch
とtestRegex
の 2 つがある。
どちらか一方のプロパティのみ指定できる。
両者の指定する正規表現は異なる。
testMatch
はmicromatch
という glob という正規表現の一種を採用している模様。
正直この正規表現はさっぱりわからん(そこに掛ける時間がない)いじることができないので公式に乗っている指定方法をそのまま踏襲。
複数の正規表現を渡すことで上から順番にテストファイル群を探してたどり着く仕組み。
testMatch: [
// __tests__ディレクトリ以下の`.ts`, `.tsx`, `.js`ファイルすべて
'**/__tests__/**/*.+(ts|tsx|js)',
// 先の結果のうち、`.spec`, `.test`が拡張子の前についているファイルすべて
'**/?(*.)+(spec|test).+(ts|tsx|js)',
// 先の結果のうち次のファイルを除外する
'!**/__tests__/setup-jest.js',
],
ということで各正規表現は常に前の結果のフィルタの役割になっているみたい。
上記の設定で、
__tests__/
ディレクトリ以下の、.test
,.spec
が付いた.ts
, .tsx
, .js
拡張子のファイル、ただしsetup-jest.js
ファイル以外が対象となる。
個人的に jest の setup ファイルは__tests__/
以下に置きたかったのでこのような指定方法にした。
tsconfig.jest.json を認識させる、設定する
テスト時にだけ参照する tsconfig ファイルを用意する
// jest.config.js
/** @type {import('ts-jest').JestConfigWithTsJest} */
module.exports = {
testEnvironment: 'node',
extensionsToTreatAsEsm: ['.ts', '.tsx', '.jsx'],
transform: {
'^.+\\.(ts|tsx)?$': [
'ts-jest',
{
useESM: true,
+ tsconfig: './tsconfig.jest.json'
},
],
'^.+\\.(js|jsx)$': 'babel-jest',
},
testMatch: [
'**/__tests__/**/*.+(ts|tsx|js)',
'**/?(*.)+(spec|test).+(ts|tsx|js)',
'!**/__tests__/setup-jest.js',
],
};
概ねの tsconfig.jest.json の設定はtsconfig.json
の方と同じになるはずなので、
extends
プロパティでオリジナルの設定を引っ張ってくればいい。
@babel/preset-typescript vs ts-jest
結論:型チェックしたいなら ts-jest しかありえない。
TypeScript ファイルをテストするには 2 通りあると jest の公式に書いてある。
babel の仕様として、
- babel は型チェックをしないため、本来 TypeScript が型チェックしたらエラーを出力するはずのコードでも babel はコード変換を問題なく実行させてしまう
- babel は
tsconfig.json
の変更を反映しない。 - babel は TypeScript コードをトランスパイルするだけである。
testEnvironment
test を実行するための実行環境を指定する。
デフォルトでnode
なので、ブラウザを想定した JavaScript コードをテストしたい場合変更する必要がある。
If you are building a web app, you can use a browser-like environment through jsdom instead.
jsdom
にすればいいだけではなく、実際にその実行環境を提供してくれるライブラリをインストールしておかなくてはならない。
$ yarn add --dev jest-environment-jsdom
// jest.config.js
/** @type {import('ts-jest').JestConfigWithTsJest} */
module.exports = {
- testEnvironment: 'node',
+ testEnvironment: 'jsdom',
extensionsToTreatAsEsm: ['.ts', '.tsx', '.jsx'],
transform: {
'^.+\\.(ts|tsx)?$': [
'ts-jest',
{
useESM: true,
+ tsconfig: './tsconfig.jest.json'
},
],
'^.+\\.(js|jsx)$': 'babel-jest',
},
testMatch: [
'**/__tests__/**/*.+(ts|tsx|js)',
'**/?(*.)+(spec|test).+(ts|tsx|js)',
'!**/__tests__/setup-jest.js',
],
};
jest.config.js
他の設定
/** @type {import('ts-jest').JestConfigWithTsJest} */
// export default {
module.exports = {
roots: ['<rootDir>/src', '<rootDir>/__tests__'],
testEnvironment: 'jsdom',
extensionsToTreatAsEsm: ['.ts', '.tsx', '.jsx'],
transform: {
'^.+\\.(ts|tsx)?$': [
'ts-jest',
{
useESM: true,
tsconfig: './tsconfig.jest.json',
},
],
'^.+\\.(js|jsx)$': 'babel-jest',
},
testMatch: [
'**/__tests__/**/*.+(ts|tsx|js)',
'**/?(*.)+(spec|test).+(ts|tsx|js)',
'!**/__tests__/setup-jest.js',
],
setupFilesAfterEnv: ['<rootDir>/src/__tests__/setup-jest.js'],
moduleFileExtensions: ['tsx', 'ts', 'js', 'json', 'node'],
collectCoverageFrom: ['src/**/*.{js,jsx,ts,tsx}'],
transformIgnorePatterns: ['/node_modules/.*']
};
roots
jestが探索してよいディレクトリを指定できる。
jestの探索対象はテストファイルとテストされるファイルなので、
もしもroots
を指定したい場合、そのディレクトリはテストファイルもテストされるファイルも両方収まっていないとならない
例: __tests__
はsrc
以下の場所でないディレクトリの場合
module.exports = {
roots: ['<rootDir>/src', '<rootDir>/__tests__'],
...
}
setupFilesAfterEnv
テストスイートが実行される前に実行しておきたい構成ファイルやフレームワークを列挙できるプロパティ
setupFiles
プロパティはフレームワークがインストールされる前に実行されるのと異なり、
フレームワークがインストールされた直後、かつテストスイーツが実行される前に実行したいものを指定できる。
setupFilesAfterEnv
で指定されるモジュール群は、各テスト ファイルで繰り返されるコードを対象としています。テスト フレームワークをインストールすると、モジュール内で Jest グローバル、Jest オブジェクト、および Expect にアクセスできるようになります。たとえば、jest 拡張ライブラリから追加のマッチャーを追加したり、セットアップ フックやティアダウン フックを呼び出したりできます。
たとえば@testing-library/react
などのフレームワークの API をグローバルで使いたい場合、setup-jest.js
でその設定を書けばすべてのテストに適用できるなど。
moduleFileExtensions
モジュールが使用するファイル拡張子の配列。ファイル拡張子を指定せずにモジュールが必要な場合、これらの拡張子が Jest によって左から右の順序で検索されます。
プロジェクトで最もよく使用される拡張子を左側に配置することをお勧めします。そのため、TypeScript を使用している場合は、「ts」または「tsx」、あるいはその両方を配列の先頭に移動することを検討してください。
collectCoverageFrom
コードカバレッジのレポートにどのファイルを含めるのか指定する。
デフォルトだとテスト中にロードされたすべてのファイルが対象になる。
レポートに余計なファイルが含まれる可能性はあるのでそうした場合を回避したいならば指定する。
今回はsrc/
以下の特定の拡張子のファイルのみを指定した。
2. RTL フレームワーク他を現在のテスト環境に追加、有効にする
目標:Reactファイルをテストできるようにする。
Installation
# ReactのDOMをテストするフレームワーク
$ yarn add --dev @testing-library/react
# ユーザの操作を模倣するフレームワーク
$ yarn add --dev @testing-library/user-event
# DOMの状態についてアサートできるライブラリ
$ yarn add --dev @testing-library/jest-dom
babel に JSX を JS へ変換してもらう
$ yarn add --dev @babel/preset-react
{
"presets": [
["@babel/preset-env", { "targets": { "node": "16.16.0" } }],
["@babel/preset-react", { "runtime": "automatic" }]
]
}
tscofnig.jest.json
の"react"
プロパティに"react-jsx"
を渡す
jest に JSX を認めさせるのと、import React from "react"
の記載なしでも React を認識してくれるようにする措置。
tsconfig.jest.json
:
{
"compilerOptions": {
"jsx": "react-jsx",
}
}
これを"preserve"にしていたら次のエラーが発生する。
Jest encountered an unexpected token
Jest failed to parse a file. This happens e.g. when your code or its dependencies use non-standard JavaScript syntax, or when Jest is not configured to support such syntax.
Out of the box Jest supports Babel, which will be used to transform your files into valid JS based on your Babel configuration.
By default "node_modules" folder is ignored by transformers.
# ...
これは jest が JSX って何?ってなっている状態。
なので"jsx": "react-jsx"
を 指定することで解決できる。
jsx: react
とjsx: react-jsx
の違い
jsx: react
にすると test ファイルでimport React from "react"
が必ず必要になる
jsx: react-jsx
にすると test ファイルで上記の import が不要になる
実際、jsx: react
にすると以下のエラーが発生する。
● Test suite failed to run
src/__tests__/ToggleSwitch.test.tsx:16:14 - error TS2686: 'React' refers to a UMD global, but the current file is a module. Consider adding an import instead.
16 <ToggleSwitch
~~~~~~~~~~~~
React
を参照したけれど module だから import を代わりに使えと言われる。
setup ファイルにインストールしたフレームワークを import してグローバルに API を使えるようにする
setup-jest.js
:
$ touch ./__tests__/setup-jest.js
import { configure } from '@testing-library/react';
import '@testing-library/jest-dom';
import '@testing-library/user-event';
configure({ testIdAttribute: 'data-my-test-id' });
これで、すべての@testing-library/react``@testing-library/user-event
,@testing-library/jest-dom
を使いたいテストファイルは import なしで API が使えるようになる
...はずなのだが自身の環境では使えるようになっていない。
現状個別にすべて各テストファイルに import している。
3. webpack 設定
実をいうと設定することはない。
要はバンドルにテストディレクトリ以下を含めたくないのでこれを webpack の設定なりで指定できないか調べたが、デフォルトの設定のままで問題ない。
webpack では、entry point で指定したファイルから参照できないファイルはバンドルに含まれない。
webpack はエントリーポイントからエントリーポイントから始まる依存グラフを生成しこのグラフの一部であるファイルのみがバンドルに含まれる。
また、production モードで webpack の tree shaking 機能がテストディレクトリを排除するようにしてあれば猶更必要はない。
参考:
Module オプション、exclude
の指定対象はバンドルから除外できるわけではない
参考:
exclude
というプロパティが webpack config にあるけど、これを指定すればバンドルから外れるのでは?という疑問に対する回答。
できない。
exclude
という設定は webpack のmodule
設定に追加できるプロパティだけど、
webpack のバンドルからは除外できるという設定ということではない。
たとえば以下のmodule
設定はどういう意味かというと
module: {
rules: [
{
test: /\.(js|jsx|tsx|ts)$/,
exclude: /node_modules/,
use: [
{
loader: require.resolve('babel-loader'),
options: {
presets: [
'@babel/preset-env',
'@babel/preset-typescript',
'@babel/preset-react',
],
plugins: [
isDevelopment &&
require.resolve('react-refresh/babel'),
].filter(Boolean),
},
},
],
},
babel-loader
はexclude
に指定されている/node_modules/
をトランスパイルしないよという意味である。
しかし webpack はnode_modules
をバンドルには含めるので、バンドルに含めないという意味ではない。
webpack-bundle-analyzer
をつかってバンドル結果を分析する
出力結果を視覚的にわかりやすく表示してくれるプラグイン。
webpack-bundle-analyzer
を使うとバンドルした結果を分析して
バンドル内容を視覚化したインタラクティブなツリーマップが作成してくれる。
この結果を見て予期せぬディレクトリが含まれていないか確認することはできる。
出来上がったもの
ディレクトリ構成:
.
|-- __tests__ # テストファイル群はここへ
` -- setup-jest.js
|-- jest.config.js
|-- node_modules
|-- package.json
|-- src
|-- tsconfig.jest.json
|-- tsconfig.json
|-- webpack.config.js
`-- yarn.lock
jest.config.js
:
/** @type {import('ts-jest').JestConfigWithTsJest} */
module.exports = {
roots: ['<rootDir>/src', '<rootDir>/__tests__'],
testEnvironment: 'jsdom',
extensionsToTreatAsEsm: ['.ts', '.tsx', '.jsx'],
transform: {
'^.+\\.(ts|tsx)?$': [
'ts-jest',
{
useESM: true,
tsconfig: './tsconfig.jest.json',
},
],
'^.+\\.(js|jsx)$': '<rootDir>/node_modules/babel-jest',
},
testMatch: [
'**/__tests__/**/*.+(ts|tsx|js)',
'**/?(*.)+(spec|test).+(ts|tsx|js)',
'!**/__tests__/setup-jest.js',
],
setupFilesAfterEnv: ['<rootDir>/__tests__/setup-jest.js'],
moduleFileExtensions: ['tsx', 'ts', 'js', 'json', 'node'],
collectCoverageFrom: ['src/**/*.{js,jsx,ts,tsx}'],
transformIgnorePatterns: ['/node_modules/.*'],
};
jest.config.js
:
import { configure } from '@testing-library/react';
import '@testing-library/jest-dom';
import '@testing-library/user-event';
configure({ testIdAttribute: 'data-my-test-id' });
.babelrc.json
:
{
"presets": [
["@babel/preset-env", { "targets": { "node": "16.16.0" } }],
["@babel/preset-react", { "runtime": "automatic" }]
]
}
tsconfig.jest.json
:
{
"extends": "./tsconfig.json",
"compilerOptions": {
"types": ["jest"],
"jsx": "react-jsx",
}
}
webpack.config.js
: 変更なし
package.json
:
{
"scripts": {
...
+ "test": "jest --config=jest.config.js"
},
}
これで以下のコマンドが正常に稼働すればテスト環境の導入は完了です。
$ npm run test
他
.babelrc vs babel.config.js
.babelrc は、ファイル/ディレクトリのサブセットに対して特定の変換/プラグインを実行する場合に便利です。おそらく、babel によって変換/変更されたくないサードパーティのライブラリがあるかもしれません。
babel.config.json は、プロジェクト内に単一の babel config を利用する複数のパッケージ (つまり、複数の package.json) ディレクトリがある場合に便利です。これはあまり一般的ではありません。
Project-wide configuration
とはプロジェクト全体に適用したい設定という意味
File-relative configuration
は特定のファイルに適用したい設定という意味かしら。
もしもbabel.config.json
ファイルが存在すれば、babel はその設定ファイルに基づいて動作する。
プロジェクト全体の構成ファイルは構成ファイルの物理的な場所から分離されているため、広範囲に適用する必要がある構成に最適である。
もしもコンパイル中の特定のファイルを見つけたとき、babel は.babelrc.json
に基づいてコンパイル処理を決定する。
なので、特定のファイルやサブセットに対して特別に設定を設けたいときに.babelrc
を用いるべき。