使用 enzyme 測試 React Native 中的組件

從 v0.18 開始,React Native 使用 React 作為依賴項而非庫的分支版本,這表示現在可以用 enzyme 的 shallow 來搭配 React Native 中的組件。

遺憾的是,React Native 有許多環境依賴項,在沒有主機裝置的情況下很難模擬。

當你想要在一般持續整合伺服器(例如 Travis)中執行測試套件時,這可能會很難處理。

要使用 enzyme 測試 React Native,你目前需要組態一個轉接器,並載入一個模擬的 DOM。

設定接合器

在討論 React Native 接合器時,可以使用標準接合器,例如 'enzyme-adapter-react-16'

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

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

載入模擬的 DOM 與 JSDOM

在 React Native 接合器出現之前,如需使用 enzyme 的 mount,必須載入模擬的 DOM。

有些人成功使用過 react-native-mock-renderer,不過建議的方式是使用 https://github.com/tmpvar/jsdomJSDOM 文件頁面上也有 enzyme 的相關文件。

JSDOM 會允許所有你預期出現的 enzyme 行為。此方法也可以使用 Jest 擷取快照測試,不過並不建議這麼做,而且只能透過 wrapper.debug() 來支援。

在缺乏 className props 時使用 enzyme 的 find

值得注意的是,React Native 允許將 testID 道具作為選擇器,類似標準 React 中的 className

    <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

更新 setupFilesAfterEnv 中指定的檔案,在本範例中是在專案根目錄中的 setup-tests.js

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() });

建立一個單獨的測試檔案

建立一個以 enzyme.test.ts 為開頭的檔案,例如 component.enzyme.test.js

/**
 * @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,以避免隨機的 React 鍵會導致快照總是失敗。

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();
    });
  });
});