📝

nextjs + typescript + jest + enzyme の最小サンプルを動かしたい

2021/09/06に公開

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タグ内のテキストが変わるコンポーネントを作成。

src/mybutton.tsx
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__ にしなくても良い。)

__test__/setup.ts
import Enzyme from 'enzyme'
import Adapter from 'enzyme-adapter-react-16'

Enzyme.configure({adapter: new Adapter()})

テストケース作成

プロジェクトのルートから __test__/mybutton.test.tsx として、テストケースを作成。

__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