TypeScript で書いている React アプリで、ユニットテストも TypeScript で書くようにするための手順。
テスティングフレームワークはJestで、ReactコンポーネントのテストをしやすくするためにEnzymeも使う。
また、アサーションライブラリとしてPower Assertを使用する。
既に以下の記事の内容でTypeScriptが導入済みであるという前提で話を進める。
この記事で出てくるライブラリのバージョンは以下。
- jest@24.8.0
- babel-jest@24.8.0
- enzyme@3.9.0
- enzyme-adapter-react-16@1.13.2
- react-test-renderer@16.8.6
- power-assert@1.6.1
- babel-preset-power-assert@3.0.0
- typescript@3.4.5
- ts-jest@24.0.2
- @types/jest@24.0.13
- @types/enzyme@3.9.3
- eslint@5.16.0
- @typescript-eslint/parser@1.9.0
- @typescript-eslint/eslint-plugin@1.9.0
テスト対象のコンポーネントを作る
propsとしてtextを受け取り、その値を表示するコンポーネント。textの型はstringに指定している。
import React from 'react';
type Props = {
text: string;
};
const App: React.FC<Props> = ({text}) => <>{`Props is ${text}.`}</>;
export default App;
<App text="foo" />とすると、Props is foo.と表示される。このコンポーネントをテストする。
JavaScript でテストを書く
説明を分かりやすくため、段階的に進めていく。まずは JavaScript でテストを書き、それを TypeScript に置き換えることにする。
必要なライブラリをインストールする。
$ yarn add -D jest babel-jest enzyme enzyme-adapter-react-16 react-test-renderer
Enzymeはv3からアダプタが必要になり、そのための設定の記述も必須になった。
アダプタのインストールは上記で済ませているので(enzyme-adapter-react-16)、次は設定の記述。
テストの設定ファイルとしてルートディレクトリにtest.config.jsを作り、以下のように記述する。
これでアダプタが有効になる。
import Enzyme from 'enzyme'; import Adapter from 'enzyme-adapter-react-16'; Enzyme.configure({adapter: new Adapter()});
そしてJestの設定ファイルとしてルートディレクトリにjest.config.jsを作り、テスト実行時にtest.config.jsを読み込むようにする。
module.exports = { setupFilesAfterEnv: ['<rootDir>/test.config.js'], };
これで設定は完了。あとはApp.test.jsとして以下のテストコードを書いて$ yarn jestを実行すると、テストが実行されて無事にパスする。
import React from 'react'; import {shallow} from 'enzyme'; import assert from 'assert'; import App from '../App'; describe('App', () => { it('表示する文字列に props.text を含む', () => { const wrapper = shallow(<App text="hogehoge" />); assert(wrapper.text().includes('hogehoge')); }); });
Power Assert の導入
Power Assertを導入しないならここは飛ばしてもよい。
エラーメッセージが分かりやすくなるため個人的には必ず入れているが。
必要なライブラリは以下の2つ。
$ yarn add -D power-assert babel-preset-power-assert
babel.config.jsに以下の内容を追加して、テスト実行時にのみbabel-preset-power-assertが有効になるようにしている。
if (process.env.NODE_ENV === 'test') { presets.push(['power-assert']); }
これで導入完了。試しにわざとテストを失敗させると、次のように出力される。
● App › 表示する文字列に props.text を含む
assert.equal(received, expected) or assert(received)
Expected value to be equal to:
true
Received:
false
Message:
# src/__tests__/App.test.js:10
assert(wrapper.text().includes('foo'))
| | |
| | false
| "Props is hogehoge."
ShallowWrapper{}
8 | it('表示する文字列に props.text を含む', () => {
9 | const wrapper = shallow(<App text="hogehoge" />);
> 10 | assert(wrapper.text().includes('foo'));
| ^
11 | });
12 | });
13 |
これで JavaScript ファイルとしてはテストコードを書けたので、次はこのコードを TypeScript に置き換える。
TypeScript でテストコードを書き、ユニットテスト時に型チェックも行うようにする
テストファイルの拡張子を.tsxにする。
$ git mv src/__tests__/App.test.js src/__tests__/App.test.tsx
次に、TypeScript をトランスパイルするためにts-jestを導入する。
インストールして、設定を記述する。
$ yarn add -D ts-jest
設定については、公式サイトが分かりやすく説明している。
JavaScript ファイルについてはbabel-jestでトランスパイルさせたいので、presetをts-jest/presets/js-with-babelで指定する。
ts-jest/presets/js-with-babel TypeScript files will be handled by ts-jest, and JavaScript files will be handled by babel-jest.
記述場所はjest.config.js。
diff --git a/jest.config.js b/jest.config.js index f3b8536..5f1bb3f 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,3 +1,4 @@ module.exports = { setupFilesAfterEnv: ['<rootDir>/test.config.js'], + preset: 'ts-jest/presets/js-with-babel', };
最後に、jestとenzymeの型宣言ファイルをインストールする。
$ yarn add -D @types/jest @types/enzyme
これで完了。
確認のため、わざとtextにnumberを渡してテストしてみる。
diff --git a/src/__tests__/App.test.tsx b/src/__tests__/App.test.tsx index 52dd85b..acd1b3b 100644 --- a/src/__tests__/App.test.tsx +++ b/src/__tests__/App.test.tsx @@ -6,7 +6,7 @@ import App from '../App'; describe('App', () => { it('表示する文字列に props.text を含む', () => { - const wrapper = shallow(<App text="hogehoge" />); - assert(wrapper.text().includes('hogehoge')); + const wrapper = shallow(<App text={1} />); + assert(wrapper.text().includes('1')); }); });
assertの条件は満たしているが型が違うので、エラーを出してくれる。
src/__tests__/App.test.tsx:9:34 - error TS2322: Type 'number' is not assignable to type 'string'.
ESLint の誤検知に対応する
張り切ってテストコードに型を書いていくと、ある問題が発生する。
diff --git a/src/__tests__/App.test.tsx b/src/__tests__/App.test.tsx index 52dd85b..2564664 100644 --- a/src/__tests__/App.test.tsx +++ b/src/__tests__/App.test.tsx @@ -1,12 +1,12 @@ import React from 'react'; -import {shallow} from 'enzyme'; +import {shallow, ShallowWrapper} from 'enzyme'; import assert from 'assert'; import App from '../App'; describe('App', () => { it('表示する文字列に props.text を含む', () => { - const wrapper = shallow(<App text="hogehoge" />); + const wrapper: ShallowWrapper = shallow(<App text="hogehoge" />); assert(wrapper.text().includes('hogehoge')); }); });
wrapperに対してShallowWrapperという型をつけたが、そのことで ESLint がエラーを出すようになってしまった。
no-unused-varsが誤検知してしまっている。
2:18 error 'ShallowWrapper' is defined but never used no-unused-vars
いずれライブラリ側でこの問題が修正されるかもしれないが、この誤検知を消すのはすぐに出来るので対応する。
なお、ESLint の TypeScript 対応の基本的な設定はこちらを参照。
@typescript-eslint/eslint-pluginが必要になるのでインストールする。
$ yarn add -D @typescript-eslint/eslint-plugin
.eslintrcを編集。
先程インストールしたプラグインを追加して、no-unused-varsのルールを一度無効にして、改めて有効にしている。
diff --git a/.eslintrc b/.eslintrc index 09486d6..33a6430 100644 --- a/.eslintrc +++ b/.eslintrc @@ -1,6 +1,9 @@ { "extends": "numb", "parser": "@typescript-eslint/parser", + "plugins": [ + "@typescript-eslint" + ], "settings": { "import/resolver": { "node": { @@ -13,5 +16,9 @@ ] } } + }, + "rules": { + "no-unused-vars": "off", + "@typescript-eslint/no-unused-vars": "error" } }
これで直る。
以下の記事を参考にした。
Babel plugins を有効にする
Power Assertを導入している場合は、まだ問題がある。
テストを失敗させると分かるが、Power Assertが有効になっていない。
これは、Babel plugins が有効になっていないため。
設定で有効に出来るので、対応する。
これについても、公式サイトの説明が分かりやすい。
diff --git a/jest.config.js b/jest.config.js index 5f1bb3f..836c286 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,4 +1,9 @@ module.exports = { setupFilesAfterEnv: ['<rootDir>/test.config.js'], preset: 'ts-jest/presets/js-with-babel', + globals: { + 'ts-jest': { + babelConfig: true, + }, + }, };