きみはねこみたいなにゃんにゃんなまほう

ねこもスクリプトをかくなり

enzyme と react-test-renderer と @testing-library/react を素朴に試す

React のテスト用ライブラリっていくつかある上に、それぞれサポートしている機能が違うんですよね。 ついでに提供されているメソッドや、メソッドチェーンの様式も様々なので、出来るだけ速やかに繰り返し実行できる環境で触りながら把握できると便利です。 Jest でコンソール出力しながらだとまどろっこしいので、手元でサクサク試せる環境をセットアップした時のメモです。

React のテスティングライブラリ

今回対象にするのは以下の3つのライブラリです。現時点の理解を書き添えてみました。

セットアップ

package.json の抜粋です。エラーを消すために色々追加していったら、こんな感じで落ち着きました。

{
  "dependencies": {
    "@testing-library/react": "^8.0.7",
    "enzyme": "^3.10.0",
    "enzyme-adapter-react-16": "^1.14.0",
    "jsdom": "^15.1.1",
    "jsdom-global": "^3.0.2",
    "nodemon": "^1.19.1",
    "react": "^16.8.6",
    "react-dom": "^16.8.6",
    "react-test-renderer": "^16.8.6"
  }
}

nodemon はファイルを監視させて再実行させるために利用しています。お試し環境の良き友ですね。

今回は省けるだけ環境構築を省く、という方針でセットアップしたので、JSX は利用できなくなっています。 小規模なら React.createElement を直接利用してもそこまで大変ではありませんでした。

それぞれ試していく

テスト対象のコンポーネント

イベントや多少のネストなんかを確認できるようにしてみました。

const { createElement: e, useState } = require('react')

// Sample Components
// -----------------

const CountButton = () => {
  const [count, setCount] = useState(0)
  return e(
    'button',
    { onClick: () => setCount(c => c + 1) },
    `count: ${count}`
  )
}

const C1 = props => e('div', {}, 'C1: ', props.content)

const C2 = () => e('div', {},
  e(C1),
  e(C1, { content: 'content-from-C2' }),
  e('div', {},
    e(CountButton),
    e(CountButton),
    e('div', {},
      e(C1)
    )
  )
)

module.exports = { CountButton, C1, C2 }

enzyme

const { createElement: e } = require('react')
const enzyme = require('enzyme')
const Adapter = require('enzyme-adapter-react-16')
const { shallow } = enzyme
const { C1, C2 } = require('./components')

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

console.log(shallow(e(C2)).debug())
console.log(shallow(e(C2)).find(C1).at(1).dive().debug())

これを実行すると

<div>
  <C1 />
  <C1 content="content-from-C2" />
  <div>
    <CountButton />
    <CountButton />
    <div>
      <C1 />
    </div>
  </div>
</div>

<div>
  C1: 
  content-from-C2
</div>

と出力されます。enzyme の shallow ってこういうことなんだなとか、 この段階ではこういうオブジェクトが帰ってきているんだなとかを眺めるための環境です。

試しに C1 内でエラーを投げるようにしてみたのですが、問題なく同じ出力がされました。

react-test-renderer

const { createElement: e, useState } = require('react')
const { create } = require('react-test-renderer')
const { C1, C2 } = require('./components')

console.log(create(e(C2)).toTree())
console.log(create(e(C2)).toJSON())

これを実行すると

{ nodeType: 'component',
  type: [Function: C2],
  props: {},
  instance: null,
  rendered:
   { nodeType: 'host',
     type: 'div',
     props: { children: [Array] },
     instance: null,
     rendered: [ [Object], [Object], [Object] ] } }

{ type: 'div',
  props: {},
  children:
   [ { type: 'div', props: {}, children: [Array] },
     { type: 'div', props: {}, children: [Array] },
     { type: 'div', props: {}, children: [Array] } ] }

@testing-library/react

動作させるには jsdom-global を実行しておくことが必要です。 Jest と組み合わせて使う場合には不要です。

const { createElement: e } = require('react')
const { render, fireEvent } = require('@testing-library/react')
require('jsdom-global')()
const { C2 } = require('./components')

const { getAllByText, debug } = render(e(C2))
debug()
fireEvent.click(getAllByText(/count/)[0])
debug()

以下のように出力されます。shallow ではないので enzyme の表示に比べて長いですね。

<body>
  <div>
    <div>
      <div>
        C1: 
      </div>
      <div>
        C1: 
        content-from-C2
      </div>
      <div>
        <button>
          count: 0
        </button>
        <button>
          count: 0
        </button>
        <div>
          <div>
            C1: 
          </div>
        </div>
      </div>
    </div>
  </div>
</body>
<body>
  <div>
    <div>
      <div>
        C1: 
      </div>
      <div>
        C1: 
        content-from-C2
      </div>
      <div>
        <button>
          count: 1
        </button>
        <button>
          count: 0
        </button>
        <div>
          <div>
            C1: 
          </div>
        </div>
      </div>
    </div>
  </div>
</body>

テスティングライブラリは大量のメソッド・ユーティリティが存在するので、TypeScript で構築すれば良かったです。