ヘルプ
サポート

Enzymeを使ったユニットテスト

AirbnbのEnzymeは、Reactコンポーネントのテストを作成するためのライブラリです。「アダプター」を使用して、Reactの異なるバージョンとReactライクなライブラリをサポートしています。PreactチームによってメンテナンスされているPreact用アダプターがあります。

Enzymeは、Karmaなどのツールを使用して通常のブラウザまたはヘッドレスブラウザで実行されるテスト、またはブラウザAPIの偽の実装としてjsdomを使用してNodeで実行されるテストをサポートしています。

Enzymeの使用法とAPIリファレンスの詳細な説明については、Enzymeのドキュメントを参照してください。このガイドの残りの部分では、PreactでEnzymeを設定する方法と、PreactでのEnzymeがReactでのEnzymeとどのように異なるかについて説明します。



インストール

EnzymeとPreactアダプターをインストールするには、次のようにします。

npm install --save-dev enzyme enzyme-adapter-preact-pure

設定

テストのセットアップコードで、Preactアダプターを使用するようにEnzymeを設定する必要があります。

import { configure } from 'enzyme';
import Adapter from 'enzyme-adapter-preact-pure';

configure({ adapter: new Adapter() });

Enzymeをさまざまなテストランナーで使用するガイダンスについては、Enzymeドキュメントのガイドセクションを参照してください。

初期値を表示する単純な`Counter`コンポーネントがあり、それを更新するためのボタンがあるとします。

import { h } from 'preact';
import { useState } from 'preact/hooks';

export default function Counter({ initialCount }) {
  const [count, setCount] = useState(initialCount);
  const increment = () => setCount(count + 1);

  return (
    <div>
      Current value: {count}
      <button onClick={increment}>Increment</button>
    </div>
  );
}

mochaやJestなどのテストランナーを使用して、期待どおりに動作することを確認するテストを作成できます。

import { expect } from 'chai';
import { h } from 'preact';
import { mount } from 'enzyme';

import Counter from '../src/Counter';

describe('Counter', () => {
  it('should display initial count', () => {
    const wrapper = mount(<Counter initialCount={5}/>);
    expect(wrapper.text()).to.include('Current value: 5');
  });

  it('should increment after "Increment" button is clicked', () => {
    const wrapper = mount(<Counter initialCount={5}/>);

    wrapper.find('button').simulate('click');

    expect(wrapper.text()).to.include('Current value: 6');
  });
});

このプロジェクトの実行可能バージョンとその他の例については、Preactアダプターのリポジトリのexamples/ディレクトリを参照してください。

Enzymeの仕組み

Enzymeは、設定されているアダプターライブラリを使用して、コンポーネントとその子コンポーネントをレンダリングします。アダプターは出力を標準化された内部表現(「React Standard Tree」)に変換します。次に、Enzymeはこれを、出力を照会し、更新をトリガーするためのメソッドを持つオブジェクトでラップします。ラッパーオブジェクトのAPIは、CSSのようなセレクターを使用して出力の一部を特定します。

完全レンダリング、シャローレンダリング、文字列レンダリング

Enzymeには、3つのレンダリング「モード」があります。

import { mount, shallow, render } from 'enzyme';

// Render the full component tree:
const wrapper = mount(<MyComponent prop="value"/>);

// Render only `MyComponent`'s direct output (ie. "mock" child components
// to render only as placeholders):
const wrapper = shallow(<MyComponent prop="value"/>);

// Render the full component tree to an HTML string, and parse the result:
const wrapper = render(<MyComponent prop="value"/>);
  • `mount`関数は、コンポーネントとそのすべての子孫を、ブラウザでレンダリングされるのと同じ方法でレンダリングします。

  • `shallow`関数は、コンポーネントによって直接出力されるDOMノードのみをレンダリングします。子コンポーネントはすべて、子のみを出力するプレースホルダーに置き換えられます。

    このモードの利点は、子コンポーネントの詳細に依存したり、すべての依存関係を構築したりすることなく、コンポーネントのテストを作成できることです。

    `shallow`レンダリングモードは、Reactと比較して、Preactアダプターでは内部的に動作が異なります。詳細については、以下の「違い」セクションを参照してください。

  • `render`関数(Preactの`render`関数と混同しないでください!)は、コンポーネントをHTML文字列にレンダリングします。これは、サーバーでのレンダリングの出力をテストしたり、エフェクトをトリガーせずにコンポーネントをレンダリングしたりする場合に役立ちます。

`act`を使った状態の更新とエフェクトのトリガー

前の例では、`.simulate('click')`を使用してボタンをクリックしました。

Enzymeは、`simulate`の呼び出しがコンポーネントの状態を変更したり、エフェクトをトリガーしたりする可能性が高いことを認識しているため、`simulate`が戻る直前に状態の更新またはエフェクトを適用します。Enzymeは、`mount`または`shallow`を使用してコンポーネントが最初にレンダリングされたとき、および`setProps`を使用してコンポーネントが更新されたときにも同じことを行います。

ただし、イベントハンドラ(例:ボタンの`onClick`プロップ)を直接呼び出すなど、Enzymeメソッド呼び出しの外部でイベントが発生した場合、Enzymeは変更を認識しません。この場合、テストは状態の更新とエフェクトの実行をトリガーし、Enzymeに出力のビューを更新するように要求する必要があります。

  • 状態の更新とエフェクトを同期的に実行するには、`preact/test-utils`の`act`関数を使用して、更新をトリガーするコードをラップします。
  • レンダリングされた出力のEnzymeのビューを更新するには、ラッパーの`.update()`メソッドを使用します。

たとえば、`simulate`メソッドを使用する代わりに、ボタンの`onClick`プロップを直接呼び出すように変更された、カウンターをインクリメントするためのテストの別のバージョンを次に示します。

import { act } from 'preact/test-utils';
it('should increment after "Increment" button is clicked', () => {
    const wrapper = mount(<Counter initialCount={5}/>);
    const onClick = wrapper.find('button').props().onClick;

    act(() => {
      // Invoke the button's click handler, but this time directly, instead of
      // via an Enzyme API
      onClick();
    });
    // Refresh Enzyme's view of the output
    wrapper.update();

    expect(wrapper.text()).to.include('Current value: 6');
});

ReactでのEnzymeとの違い

一般的な意図は、Enzyme + Reactを使用して記述されたテストを、Enzyme + Preactで簡単に動作させることができるようにすることです。またはその逆も可能です。これにより、Preact用に最初に記述されたコンポーネントをReactで動作するように切り替える必要がある場合に、すべてのテストを書き直す必要がなくなります。またはその逆も可能です。

ただし、このアダプターとEnzymeのReactアダプターの間には、注意すべき動作の違いがいくつかあります。

  • 「シャロー」レンダリングモードは、内部的には動作が異なります。Reactと同様に、コンポーネントを「1レベルの深さ」でのみレンダリングしますが、Reactとは異なり、実際のDOMノードを作成します。また、通常のライフサイクルフックとエフェクトもすべて実行します。
  • `simulate`メソッドは実際のDOMイベントをディスパッチしますが、Reactアダプターでは、`simulate`は`on<EventName>`プロップを呼び出すだけです。
  • Preactでは、状態の更新(例:`setState`の呼び出し後)はまとめてバッチ処理され、非同期的に適用されます。Reactでは、コンテキストに応じて、状態の更新をすぐに適用したり、バッチ処理したりできます。テストの作成を容易にするために、Preactアダプターは、アダプターで`setProps`または`simulate`呼び出しを介してトリガーされた初期レンダリングと更新後に状態の更新とエフェクトをフラッシュします。他の方法で状態の更新またはエフェクトがトリガーされた場合、テストコードは`preact/test-utils`パッケージの`act`を使用して、エフェクトと状態の更新のフラッシュを手動でトリガーする必要がある場合があります。

詳細については、PreactアダプターのREADMEを参照してください。