在 React Native 中使用 enzyme 测试组件

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

使用 JSDOM 加载模拟 DOM

要在 React Native 适配器出现之前使用 enzyme 的 mount,必须加载模拟 DOM。

虽然有些人成功使用了 react-native-mock-renderer,但建议的方法是使用 https://github.com/tmpvar/jsdom,如 JSDOM 文档页面中为 enzyme 文档所示。

JSDOM 将允许所有您期望的 enzyme 行为。虽然 Jest 快照测试也可以与此方法一起使用,但不鼓励这样做,并且仅通过 wrapper.debug() 提供支持。

在缺少 className 属性时使用 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();
    });
  });
});