React NativeコンポーネントのテストにEnzymeを使用する

v0.18以降、React Nativeはフォークされたバージョンのライブラリではなく、依存関係としてReactを使用しています。そのため、Enzymeの`shallow`をReact Nativeコンポーネントで使用できるようになりました。

残念ながら、React Nativeには多くの環境依存関係があり、ホストデバイスなしでシミュレートするのは困難です。

これは、Travisなどの一般的な継続的インテグレーションサーバーでテストスイートを実行したい場合に困難になります。

React NativeをテストするためにEnzymeを使用するには、現在、アダプターを設定し、エミュレートされたDOMをロードする必要があります。

アダプターの設定

React Nativeアダプターは議論中ですが、'enzyme-adapter-react-16'などの標準アダプターを使用できます。

import Adapter from 'enzyme-adapter-react-16';

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

JSDOMによるエミュレートされたDOMのロード

React Nativeアダプターが存在するまでEnzymeの`mount`を使用するには、エミュレートされたDOMをロードする必要があります。

react-native-mock-rendererで成功した例もありますが、推奨される方法はEnzymeのJSDOMドキュメントページで説明されているようにhttps://github.com/tmpvar/jsdomを使用することです。

JSDOMを使用すると、期待どおりのすべての`enzyme`の動作が可能になります。このアプローチではJestのスナップショットテストも使用できますが、推奨されておらず、`wrapper.debug()`でのみサポートされています。

classNameプロパティがない場合のEnzymeのfindの使用

React Nativeでは、標準Reactの`className`と同様のセレクターとして使用できるtestIDプロップが許可されていることに注意してください。

    <View key={key} style={styles.todo} testID="todo-item">
      <Text testID="todo-title" style={styles.title}>{todo.title}</Text>
    </View>
expect(wrapper.findWhere((node) => node.prop('testID') === 'todo-item')).toExist();

JestとJSDOMの置換のためのデフォルトの例の設定

テストフレームワークに必要な設定を実行するには、Jestの`setupFilesAfterEnv`設定などを使用してセットアップスクリプトを使用することをお勧めします。

プロジェクトのルートに`jest.config.js`ファイルを作成するか、更新して`setupFilesAfterEnv`設定を含めます。

// jest.config.js

module.exports = {
  // Load setup-tests.js before test execution
  setupFilesAfterEnv: '<rootDir>setup-tests.js',

  // ...
};

次に、`setupFilesAfterEnv`で指定されたファイル(この場合はプロジェクトルートの`setup-tests.js`)を作成するか、更新します。

// setup-tests.js

import 'react-native';
import 'jest-enzyme';
import Adapter from 'enzyme-adapter-react-16';
import Enzyme from 'enzyme';

/**
 * Set up DOM in node.js environment for Enzyme to mount to
 */
const { JSDOM } = require('jsdom');

const jsdom = new JSDOM('<!doctype html><html><body></body></html>');
const { window } = jsdom;

function copyProps(src, target) {
  Object.defineProperties(target, {
    ...Object.getOwnPropertyDescriptors(src),
    ...Object.getOwnPropertyDescriptors(target),
  });
}

global.window = window;
global.document = window.document;
global.navigator = {
  userAgent: 'node.js',
};
copyProps(window, global);

/**
 * Set up Enzyme to mount to DOM, simulate events,
 * and inspect the DOM in tests.
 */
Enzyme.configure({ adapter: new Adapter() });

他のテストライブラリでenzymeを設定し、動的にJSDOMを含める

プロジェクトルートの`setup-tests.js`など、`setupFilesAfterEnv`で指定されたファイルを更新します。

import 'react-native';
import 'jest-enzyme';
import Adapter from 'enzyme-adapter-react-16';
import Enzyme from 'enzyme';

/**
 * Set up Enzyme to mount to DOM, simulate events,
 * and inspect the DOM in tests.
 */
Enzyme.configure({ adapter: new Adapter() });

別のテストファイルを作成する

例えば`component.enzyme.test.js`のように、`enzyme.test.ts`で始まるファイルを作成します。

/**
 * @jest-environment jsdom
 */
import React from 'react';
import { mount } from 'enzyme';
import { Text } from '../../../component/text';

describe('Component tested with airbnb enzyme', () => {
  test('App mount with enzyme', () => {
    const wrapper = mount(<Text />);
    // other tests operations
  });
});

最も重要なのは、テストが`jestEnvironment`を`jsdom`に設定して実行されることを確認することです - ファイルの先頭に`/* @jest-environment jsdom */`コメントを含める方法があります。

その後、テストの記述を開始できるはずです!

ネイティブコンポーネントの追加モックを実行する場合、またはReact Nativeコンポーネントに対してスナップショットテストを実行したい場合は、追加のモックが必要になる場合があります。この場合、スナップショットが常に失敗するのを回避するために、React Navigationの`KeyGenerator`をモックする必要があることに注意してください。

import React from 'react';
import renderer from 'react-test-renderer';
import { mount, ReactWrapper } from 'enzyme';
import { Provider } from 'mobx-react';
import { Text } from 'native-base';

import { TodoItem } from './todo-item';
import { TodoList } from './todo-list';
import { todoStore } from '../../stores/todo-store';

// https://github.com/react-navigation/react-navigation/issues/2269
// React Navigation generates random React keys, which makes
// snapshot testing fail. Mock the randomness to keep from failing.
jest.mock('react-navigation/src/routers/KeyGenerator', () => ({
  generateKey: jest.fn(() => 123),
}));

describe('todo-list', () => {
  describe('enzyme tests', () => {
    it('can add a Todo with Enzyme', () => {
      const wrapper = mount(
        <Provider keyLength={0} todoStore={todoStore}>
          <TodoList />
        </Provider>,
      );

      const newTodoText = 'I need to do something...';
      const newTodoTextInput = wrapper.find('Input').first();
      const addTodoButton = wrapper
        .find('Button')
        .findWhere((w) => w.text() === 'Add Todo')
        .first();

      newTodoTextInput.props().onChangeText(newTodoText);

      // Enzyme usually allows wrapper.simulate() alternatively, but this doesn't support 'press' events.
      addTodoButton.props().onPress();

      // Make sure to call update if external events (e.g. Mobx state changes)
      // result in updating the component props.
      wrapper.update();

      // You can either check for a testID prop, similar to className in React:
      expect(
        wrapper.findWhere((node) => node.prop('testID') === 'todo-item'),
      ).toExist();

      // Or even just find a component itself, if you broke the JSX out into its own component:
      expect(wrapper.find(TodoItem)).toExist();

      // You can even do snapshot testing,
      // if you pull in enzyme-to-json and configure
      // it in snapshotSerializers in package.json
      expect(wrapper.find(TodoList)).toMatchSnapshot();
    });
  });
});