nextjs + typescript + jest + enzyme の最小サンプルを動かしたい
nextjs + typescript + jest + enzyme の組み合わせで、UIを操作したテストを動かしたいと思います。
先人の詳しい記事がたくさんあるのですが、リッチなサンプルが多かったので、さらっと動かして確認したいときのための最小サンプルをメモしておきたいと思います。
やりたいこと
- ボタンを押したらテキストが変わるコンポーネントを作る。
- ボタンを押す前、押した後のテキストの内容を、自動テストするサンプルを、Jest + enzyme で作りたい
動かした環境
- macOS 11.5.2
- Node.js 14.17.0
- yarn 1.22.10
後述の create-next-app で自動的に入ったライブラリのバージョン
- react 17.0.2
- nextjs 11.1.2
nextjsアプリを作成
typescriptでnextjsアプリを作成。
npx create-next-app --ts jestsample
必要なライブラリの導入
create-next-appでreact17が入ってしまっているが、enzyme adapterのreact17.x用のものはまだ無いようなので、16.x用を使う。
(yarn add をすると、警告が出ているが、今回は無視)
yarn add jest ts-jest enzyme enzyme-adapter-react-16 enzyme-to-json @types/jest @types/enzyme-adapter-react-16 -D
ライブラリ | |
---|---|
jest | Jest本体 |
ts-jest | TypescriptでJestを動かすため |
enzyme | UI操作のテストをするため |
enzyme-adapter-react-16 | enzymeを使うとき、使用するUIライブラリのバージョンに合わせたアダプタが必要 |
@types/jest | Jestの型定義 |
@types/enzyme-adapter-react-16 | enzymeアダプタの型定義 |
※コンポーネントの定義をスナップショットで取るような機能を使う場合、enzyme-to-json ライブラリなども必要そうだが、今回の、ボタンクリックを自動で動かすだけのサンプルでは必要なかった。
Jestの設定ファイルを作成
jest --init を実行し、質問に答えていくと、jest.config.js が作成される。
(このセクションの最後に、最終的なjest.config.jsがあります)
yarn jest --init
? Would you like to use Typescript for the configuration file? › (y/N) ← Nを入力
? Choose the test environment that will be used for testing › - Use arrow-keys. Return to submit.
❯ node ←こちら
jsdom (browser-like)
? Do you want Jest to add coverage reports? › (y/N) ←レポートが欲しければ y
? Which provider should be used to instrument code for coverage? › - Use arrow-keys. Return to submit.
❯ v8 ← 今回はとりあえずこちら
babel
? Automatically clear mock calls and instances between every test? › (y/N) ← 今回はNにしておく
作成された jest.config.js を開き、下記の部分をそれぞれ変更する。
globals:
2019年のissueだが、今もこれを入れないと動かない。
(tsconfig.jest.json はこのあと作成する。)
globals: {
'ts-jest': {
tsconfig: '<rootDir>/__test__/tsconfig.jest.json',
},
},
これがないと、テスト実行時、
var component = (0, enzyme_1.shallow)(<mybutton_1.default />);
^
SyntaxError: Unexpected token '<'
のようなエラーになる。
moduleFileExtensions:
コメントを外して、この拡張子を有効にしておく。
moduleFileExtensions: [
'js',
'json',
'jsx',
'node',
'ts',
'tsx',
],
preset:
今回はBabelは使わないで ts-jest でコンパイル。
preset: 'ts-jest',
setupFilesAfterEnv:
enzymeの初期処理を呼び出す。
setupFilesAfterEnv: [
'<rootDir>/__test__/setup.ts',
],
testMatch:
テスト対象として扱うファイルパターンを指定。
testMatch: [
'**/?(*.)+(spec|test).[tj]s?(x)',
'**/__tests__/**/*.[jt]s?(x)',
],
最終的な jest.config.js
最終的にコメントを削るとこのようになる。
module.exports = {
collectCoverage: true,
coverageDirectory: "coverage",
coverageProvider: "v8",
globals: {
'ts-jest': {
tsconfig: '<rootDir>/__test__/tsconfig.jest.json',
},
},
moduleFileExtensions: [
'js',
'json',
'jsx',
'node',
'ts',
'tsx',
],
preset: 'ts-jest',
setupFilesAfterEnv: [
'<rootDir>/__test__/setup.ts',
],
testMatch: [
'**/?(*.)+(spec|test).[tj]s?(x)',
'**/__tests__/**/*.[jt]s?(x)',
],
};
issueに関連した jest用のconfigを作成
プロジェクトのルートから __test__/tsconfig.jest.json を作成し、以下の内容にする。
{
"extends": "../tsconfig.json",
"compilerOptions": {
"jsx": "react"
}
}
テスト用のコンポーネントを作る
src/mybutton.tsx として、ボタンを押したらpタグ内のテキストが変わるコンポーネントを作成。
import React, { useState } from 'react'
const MyButton = () => {
const [label, setLabel] = useState('--')
return (
<div>
<p id='label'>{label}</p>
<button id='btn' onClick={() => setLabel('OK')}>ClickMe</button>
</div>
)
}
export default MyButton
Enzymeの初期設定を作成
プロジェクトのルートから __test__/setup.ts として、初期設定処理を作成。
(設定ファイルで指定できるので、ディレクトリは __test__ にしなくても良い。)
import Enzyme from 'enzyme'
import Adapter from 'enzyme-adapter-react-16'
Enzyme.configure({adapter: new Adapter()})
テストケース作成
プロジェクトのルートから __test__/mybutton.test.tsx として、テストケースを作成。
import { shallow } from "enzyme"
import React from "react"
import MyButton from "../src/mybutton"
test('mybutton test', () => {
const component = shallow(<MyButton />)
// pタグのテキストを確認。find({id: 'label'}) でも良い
expect(component.find('p').text()).toEqual('--')
// ボタンをクリック。find({id: 'btn'}) でも良い
component.find('button').simulate('click')
// もう1度テキストを取り直す
expect(component.find('p').text()).toEqual('OK')
})
package.json にテスト用のコマンド作成
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint",
"test": "jest"
},
テスト実行
yarn test
このような結果になる
% yarn test
yarn run v1.22.10
$ jest
PASS __test__/mybutton.test.tsx
✓ mybutton test (9 ms)
--------------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
--------------|---------|----------|---------|---------|-------------------
All files | 100 | 100 | 50 | 100 |
mybutton.tsx | 100 | 100 | 50 | 100 |
--------------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 3.02 s
Ran all test suites.
✨ Done in 4.38s.
ためしに、テストケースで toEqual('xx') のように変更してテストを失敗させると、このようになる。
% yarn test
yarn run v1.22.10
$ jest
FAIL __test__/mybutton.test.tsx
✕ mybutton test (12 ms)
● mybutton test
expect(received).toEqual(expected) // deep equality
Expected: "xx"
Received: "--"
6 | const component = shallow(<MyButton />)
7 | // pタグのテキストを確認。find({id: 'label'}) でも良い
> 8 | expect(component.find('p').text()).toEqual('xx')
| ^
9 | // ボタンをクリック。find({id: 'btn'}) でも良い
10 | component.find('button').simulate('click')
11 | // もう1度テキストを取り直す
at Object.<anonymous> (__test__/mybutton.test.tsx:8:38)
--------------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
--------------|---------|----------|---------|---------|-------------------
All files | 100 | 100 | 50 | 100 |
mybutton.tsx | 100 | 100 | 50 | 100 |
--------------|---------|----------|---------|---------|-------------------
Test Suites: 1 failed, 1 total
Tests: 1 failed, 1 total
Snapshots: 0 total
Time: 4.427 s, estimated 5 s
Ran all test suites.
error Command failed with exit code 1.
Discussion