Reactでレンダリング回数やレンダリングにかかる時間をユニットテストでテストできるライブラリを開発しました

先日、[react-performance-testing](https://github.com/keiya01/react-performance-testing) という React でレンダリング回数やレンダリングにかかる時間をテストできるライブラリを開発しました🎉

Reactをテストする時にレンダリング回数をテストしたいと思ったことがある人は多いはずです。実際、パフォーマンスを意識して開発しているときや、動作が重くなりやすいコンポーネントを開発している時には、レンダリング回数をテストして、自動で確認できるようにした方が開発効率はよくなります。今回開発した [react-performance-testing](https://github.com/keiya01/react-performance-testing) はそのような課題を解決し、React におけるランタイムのパフォーマンスをユニットテストで簡単にテストできるライブラリーです。

## 使い方

### インストール

npm:

```bash
npm install --save-dev react-performance-testing
```

yarn:

```bash
yarn add --dev react-performance-testing
```

### レンダリング数をテストする

レンダリング数をテストするための基本的な使い方は、以下のような実装になります。

```jsx
import React from 'react';
import { render, fireEvent, screen } from '@testing-library/react';
import { perf, wait } from 'react-performance-testing';

test('countボタンをクリックした時に、2回レンダリングする', async () => {
  const Button = ({ name, onClick }) => (
    <button type="button" onClick={onClick}>{name}</button>
  );
  const Counter = () => {
    const [count, setCount] = React.useState(0);
    return (
      <div>
        <span>{count}</span>
        <Button name="count" onClick={() => setCount(c => c + 1)}/>
      </div>
    );
  };

  // perf メソッドの内部で createElement にモンキーパッチを当てているので、
  // React モジュールを引数に渡す必要があります。
  const { renderCount } = perf(React);

  fireEvent.click(screen.getByRole('button', { name: /count/i }));

  // renderCount.current 以下に各コンポーネントの名前がkeyとなるオブジェクトを含んでいて、
  // さらにその中の value プロパティにレンダリング数が含まれています。
  await wait(() => {
    expect(renderCount.current.Counter.value).toBe(2);
    expect(renderCount.current.Button.value).toBe(2);
  });
});
```

上記のような形でレンダリング数をテストすることができます。

### レンダリングにかかる時間をテストする

レンダリングにかかる時間をテストするための基本的な使い方は、以下のような実装になります。

```jsx
import React from 'react';
import { render, fireEvent, screen } from '@testing-library/react';
import { perf } from 'react-performance-testing';

test('countボタンをクリックした時に、レンダリングにかかる時間が１６ミリ秒以下になる', async () => {
  const Button = ({ name, onClick }) => (
    <button type="button" onClick={onClick}>{name}</button>
  );
  const Counter = () => {
    const [count, setCount] = React.useState(0);
    return (
      <div>
        <span>{count}</span>
        <Button name="count" onClick={() => setCount(c => c + 1)}/>
      </div>
    );
  };

  const { renderTime } = perf(React);

  fireEvent.click(screen.getByRole('button', { name: /count/i }));

  await wait(() => {
    // renderCount.current 以下に各コンポーネントの名前がkeyとなるオブジェクトを含んでいて、
    // さらにその中の mount プロパティと updates プロパティにレンダリングにかかる時間が含まれています。
    // 値はミリ秒単位で渡されます。
    expect(renderTime.current.Counter.mount).toBeLessThan(16); // 初期レンダリング
    // updatesは配列
    expect(renderTime.current.Counter.updates[0]).toLessThan(16); // state更新
  
});
```

上記のような形でレンダリングにかかる時間をテストできます。**渡される値はミリ秒単位であることに注意してください**。

`mount`プロパティには初期レンダリング(マウント時)の時間を計測した結果が含まれます。

`updates`プロパティは配列で渡されます。このプロパティにはstateを更新した時などに発生する、再レンダリングにかかる時間の計測結果が格納されています。

ここでの課題としては`updates`プロパティをテストする時の体験がいまいちなところです。これに関しては、今後Utilメソッドのようなものを開発するか、[@testing-library/jest-dom](https://github.com/testing-library/jest-dom) のような、`jest`を拡張するライブラリを開発する予定です。

**注意**: `renderTime`を使う場合、テストは一つずつ行う必要があることに注意してください。計測プロセスには、Inline CachingというV8の最適化処理が入ってしまうため、最初のテストケースの結果と２回目のテストケースの結果が大きく変わってしまう可能性があるからです。Inline Cashingについては[この記事](https://blog.sessionstack.com/how-javascript-works-inside-the-v8-engine-5-tips-on-how-to-write-optimized-code-ac089e62b12e)がわかりやすいです。

### 複数のコンポーネントを含む場合

複数のコンポーネントを含む場合は、`renderCount`と`renderTime`に含まれるコンポーネントは表示される順に配列で渡されます。

```jsx
const Text = ({ text }) => <p>{text}</p>;
const Component = () => (
  <div>
    <Text text="1"/>
    <Text text="2"/>
    <Text text="3"/>
  </div>
);
const { renderCount } = perf(React);

wait(() => console.log(renderCount.current)); 
/* 
* { 
*   Component: { value: 1 },
*   Text: [{ value: 1 }, { value: 1 }, { value: 1 }]
* }
*/
```

複数のコンポーネントを含む場合、上記のように配列で渡されます。そして、各コンポーネントのプロパティはネストしているコンポーネントを区別せずにフラットな状態で渡されます。これは、このライブラリが小さな単位(ListやModalなど)でコンポーネントをテストするために作られたものだからです。小さなコンポーネントをテストするライブラリであるため、ネストした状態で値を保持するのは、コード量が増えたり、テストしにくくなってしまうと考えたためこのような設計になっています。

例えば、

```js
expect(renderCount.current.Component.value).toBe(1);
expect(renderCount.current.Component.Text[0].value).toBe(1);
```

とするよりも

```js
expect(renderCount.current.Component.value)toBe(1);
expect(renderCount.current.Text[0].value)toBe(1);
```

のように書けた方が、すぐにコンポーネントにアクセスできてテストを書きやすいと考えました。

そのため、Pageコンポーネントなどの多くの重複するコンポーネントを含む大きなコンポーネントをテストするケースでは使い勝手が悪いです。

しかし、今後、大きなコンポーネントでも開発体験を向上できるように改善していく予定です。

良い方法があればissueまたはPRを送っていただければ嬉しいです:bow:

### 推奨テストライブラリ

[react-performance-testing](https://github.com/keiya01/react-performance-testing) を使う時に他のテストライブラリと併用して使うと思います。

Jest, Mocha, Jasmine では自動でクリーンアップなどの処理を行うため、これらのライブラリを合わせて使うことをお勧めします。これ以外のライブラリを使っている場合は、teardownのタイミングで [react-performance-testing](https://github.com/keiya01/react-performance-testing) の`cleanup`メソッドを実行してください。

## TypeScript

TypeScript にも対応していて、型安全なテストを実装することができます。

```tsx
const Text = ({ text }) => <p>{text}</p>;
const Component = () => (
  <div>
    <Text text="1"/>
    <Text text="2"/>
    <Text text="3"/>
  </div>
);

const Component = () => <div>test</div>;
const { renderCount } = perf<{ Component: unknown, Text: unknown[] }>(React);
```

Generics を使ってコンポーネントの名前をkeyに持つ`unknown`型を定義することで、そのコンポーネントが配列になるのか、単純なオブジェクトなのかを定義できます。

この方法についてはもっと良いやり方がありそうなので、issueまたはPRお待ちしています:bow:

## ReactNative

ReactNativeでも使うことができます。

ReactNativeで使うには、

```js
import { perf } from 'react-performance-testing/native';
```

のようにインポートすることで、ReactNativeに対応した`perf`メソッドを使うことができます。

## ざっくりとした仕組み

`perf`メソッドには`React`モジュールを渡しています。これは、`React.createElement`にモンキーパッチを当てて、渡されたコンポーネントを独自の関数でラップして、レンダリング回数やレンダリングにかかる時間を計測するようにしているからです。このような仕組みであるため、`React`モジュールを引数に渡す必要があります。

## 最後に

ここまで読んでいただきありがとうございました！

是非使ってみてください。

そしてフィードバックやGitHubに:star:をつけてくれたら、モチベーションになります:muscle:

GitHub: https://github.com/keiya01/react-performance-testing

## 追記(2020/08/21)

v1.2.0 で Breaking Changes が入りました。今までは`renderCount`などの変数を直接使うことができていましたが、今後は `wait` メソッドでラップする必要があります。
変更の経緯は、これまでは値の計測および変数への代入などの処理をレンダリング処理中に行っていました。この処理はわずかな処理であるため、通常はそこまで大きな誤差にはならないのですが、大きいコンポーネントや複雑な処理を含む場合、`renderTime`に大きく影響する可能性があるため、レンダリング処理が終わったタイミングで値の計測などの処理を行うように変更しました。