🛠️

EnzymeよりReact Testing Libraryでしょ〜

2022/12/26に公開

この記事は Money Forward Engineering 1 Advent Calendar 2022 23 日目の投稿です 🎄
22 日目は コットン さんによる 『技術負債解消に取り組んだ新卒1年目を振り返ります』 でした。


備忘録がてらなぜフロントエンドテストツールとしてEnzymeではなくReact Testing Libraryを使うべきか書こうと思います。

最初に、昨今のフロントエンドテストはTesting TrophyというWebアプリケーションの効率的なテスト手法に則りテストを書いていくことが良いとされています。

Testing Torophy


ref: https://testingjavascript.com/

Testing Torophyは、アプリケーションテストをE2E,Integration,Unit,Staticという4層に分けます。
そしてそれぞれ以下のような意味を持ちます。

End to End(E2E)

ブラウザ上でユーザと同じような操作を行い、正常に動作するかを確認するテスト
基本的にE2Eテストツールを使うことになり、Autify,Cypress,MagicPodなどがこれにあたる。

Integration

複数の関数、componentなどのモジュールを組み合わせて正常に動作するかを確認するテスト
Jest, React Testing Libraryなどがこれにあたる。

Unit

単一の関数やcomponentを個別に確認するテスト
Jest, Enzymeなどがこれにあたる。

Static

Typescriptによる型チェックや、Eslint,Prettierによる静的チェック

そしてTesting Torophyの方針は、
フロントエンドは小さなUnitを組み合わせて作っていくもので、これらを組み合わせた物をUnitテストだけで動作保証することは難しい。
E2Eテストは非常に信頼性の高いテストで全体の動作を保証できるものだが、実行速度やお金などのコストの面で不利があり、全てE2Eテストでというのも難しい。

そこで信頼性、コストを考慮して最もトレードオフのバランスが良いのはIntegrationテストなので、この4種のテストの中で最も手厚くすべきなのはIntegrationテストであるとする考え方です。

こちらの記事がとても分かりやすかったので詳細を知りたい方はこちらへ。
https://qiita.com/mrnaoki/items/3fd211deb8711fae8204

そしてこのTesting Torophyの考案者であるKent C. Doddsが作成したIntegrationテストに特化したReact用テスティングライブラリーがReact Testing Libraryで、React公式でもテストツールとしてこちらが推奨されています。

React Testing Library は実装の詳細に依存せずに React コンポーネントをテストすることができるツールセットです。このアプローチはリファクタリングを容易にし、さらにアクセシビリティのベスト・プラクティスへと手向けてくれます。
https://ja.reactjs.org/docs/testing.html#tools

しかし他にもReactテストツールとしてよく名前が上がるEnzymeというライブラリがあります。
React Testing Libraryと比較してもEnzymeの方が歴史が長く、githubのstar数も多いです。

しかしEnzymeには開発者として辛い部分があるのです。

Enzymeの辛み

まずEnzymeとReact Testing Libraryはテスト方針が違います。
EnzymeはUnitテストに特化していて、stateやpropsなどの実装の詳細でテストするのに対して、
React Testing LibraryはIntegrationテストに特化しており、実際にレンダーしたDOMでユーザが行う動作に近い形でテストします。

まずEnzymeのようにstate,propsなどの実装の詳細をテストすると、コードをリファクタしたときにテストが失敗する偽陽性を孕む可能性がある。

(Kent C. DoddsのTesting Implementation Detailsではコードを破壊してもテストが失敗しない可能性がある偽陰性も孕んでいると解説されていますが、原因であるEnzymeのinstance apiはFunctional Componentでは使用できないので偽陰性に関しては説明を省きます。(もしかしたら可能性があるのかもしれませんが...))

以下のような簡単なCounterコンポーネントを例に見ていきましょう。

import React, { useState } from 'react';
import { Text } from './Text';

export const Counter = () => {
  const [count, setCount] = useState(0);
  return (
    <>
      <Text content={`Count is ${count}`} />
      <button onClick={() => setCount(count + 1)}>click Me!</button>
    </>
  );
};

実装の詳細をテストするようなテストコードはこのようになります。

Enzymeでのテスト

import React from 'react';
import { mount } from 'enzyme';

import { Counter } from './Counter';

test('click:count', () => {
  const wrapper = mount(<Counter />);
  const button = wrapper.find('button');
  let Text = wrapper.find('Text');

  expect(Text.props().content).toBe('Count is 0');
  button.simulate('click');
  Text = wrapper.find('Text');
  expect(Text.props().content).toBe('Count is 1');
});

このようにcomponentに正しいpropsが渡されているかを確認し、渡されている場合は正しいビューが反映されていると推測します。

ではここでCounterコンポーネントの簡単なリファクタをしてみます。

import React, { useState } from 'react';
import { Text } from './Text';

export const Counter = () => {
  const [count, setCount] = useState(0);
  return (
    <>
+     <Text>Count is {count}</Text>
-     <Text content={`Count is ${count}`} />
      <button onClick={() => setCount(count + 1)}>click Me!</button>
    </>
  );
};

視認性を上げるためTextコンポーネントのcontent propsをchildrenに移動してみました。
しかしこれはあくまでリファクタなので挙動上の変更はありません。

なのに、、、再びEnzymeのテストコードを走らせると落ちてしまいました。

content propsなどないと怒られるわけです。
しかしブラウザ上で確認するとCounterコンポーネントは正常に動作しています。
つまりコードは正しいがテストは落ちる、これが偽陽性です。

このようにpropsなどの実装の詳細に依存したテストを書くことは、リファクタなのにテストが落ちるなどの偽陽性を孕んでいると言えます。

では上記のCounterコンポーネントのテストをReact Testing Libraryで書くとどうなるでしょうか。

React Testing Libraryでのテスト

import React from 'react';
import { fireEvent, render } from '@testing-library/react';
import { Counter } from './Counter';

test('click:count', () => {
  const container = render(<Counter />);
  const button = container.getByRole('button');

  expect(container.getByText('Count is 0')).toBeInTheDocument();
  fireEvent.click(button);
  expect(container.getByText('Count is 1')).toBeInTheDocument();
});

React Testing Libraryはユーザ目線でテストすることを思想としているので、このように実装の詳細に依存せず、ユーザ目線で正しく動作しているかを確認するようなテストになります

では再びCounterコンポーネントのリファクタを行います。

export const Counter = () => {
  const [count, setCount] = useState(0);
  return (
    <>
+     <Text>Count is {count}</Text>
-     <Text content={`Count is ${count}`} />
      <button onClick={() => setCount(count + 1)}>click Me!</button>
    </>
  );
};

無事Passしました
Test Suites: 1 passed, 1 total

このように実装の詳細に依存していないテストを書くことは、リファクタを安心して行えることにつながり開発ストレスを軽減します。
またClass ComponentからFunctional Componentへの移行も楽になります。

まとめ

EnzymeよりReact Testing Libraryを使うべき理由は大きくまとめて以下2つです。

  • アプリケーションの効率的なテスト手法であるTesting Trophyに則ったIntegrationテストに特化したテストツールであること
  • 実装の詳細に依存しないテストを書くことで開発者のストレス軽減につながること

そしてEnzymeはReact17、18を公式にはサポートしていないので、そちらを踏まえても現環境ではReact Testing Libraryを使うべきと言えると思います。

Discussion