1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Test 環境を React + Webpack 環境へ導入する手順

Posted at

この手順でやれば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 のバージョンを指定する。

.babelrcbabel.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-jestuseESMを有効にする
  • 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 ファイル群を指定するプロパティはtestMatchtestRegexの 2 つがある。

どちらか一方のプロパティのみ指定できる。

両者の指定する正規表現は異なる。

testMatchmicromatchという 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: reactjsx: 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-loaderexcludeに指定されている/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を用いるべき。

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?