硅基俊宁.
Skills · 技能体系 ↗
← 返回文章

使用 Jest 和 Enzyme 进行 React Native 单元测试

俊宁 · 2023-04-19
迁自历史博客,保留原文、署名、引用与原始日期;文中的版本、成果与性能结论不代表重新完成了当前验收。
查看原始出处 ↗ · 查看迁移清单
使用 Jest 和 Enzyme 进行 React Native 单元测试封面

单元测试是什么 🧐

单元测试是用来对一个模块、一个函数或者一个类来进行正确性检验的测试工作。程序单元是应用的最小可测试部件,在 React 编程中,最小单元通常是组件、函数。如果你熟悉“测试驱动开发”(TDD:Test-Driven Development),单元测试也不会陌生,狭义来说就是单测驱动开发。

通常来说,程序员每修改一次程序就会进行最少一次单元测试,在编写程序的过程中前后很可能要进行多次单元测试,以证实程序达到软件规格书要求的工作目标,没有程序错误。在 TDD 中,甚至是先根据设计编写单元测试,然后根据单元测试写代码。

万丈高楼平地起,单元测试和文档一样,是保障程序最小单元质量的重要一环。试想一下,一块砖可能不需要使用说明书就可以量产使用,但是一块砖不经质检测验就投入使用带来的后果可能是恐怖的。从这个角度来看,单测可能是比文档更重要的存在。当然我们也不提倡为了单测而单测,单测是为了防范于未然。

其他测试

前端测试常见的测试类型有单元测试(Unit testing)、集成测试(Integration testing)、端到端(E2E testing)测试,一般我们投入的测试资源排序如下:

紫升

集成测试是在单元测试的基础上,集成多个模块进行测试,确保模块之间互动行为正确无误的工作。有时,单一的模块完全通过单元测试,单独使用也没有问题,但是当与其他模块配合使用时,可能就出现问题了,下图是未通过集成测试的例子:

原图暂不可用:紫升

端到端测试是站在用户角度出发(一端)到真实运行环境(另一端)进行测试。一般我们会使用 Cypress、puppeteer 这些工具进行自动化测试以替代人肉测试。下图是未通过端到端测试的例子:

原图暂不可用:紫升

测试覆盖率

我们在测试的时候,会经常关心我们的代码是否都测试到了,以及哪些代码没有测试到。jest 内置了 Istanbul 测试覆盖率工具,我们可以通过四个维度的覆盖率来了解代码测试覆盖率情况:

下图是执行 jest --coverage 之后生成的命令行输出:

紫升

下图是生成的精美的测试覆盖率报告:

紫升

点击 App.js 可以查看单个文件的测试覆盖率情况:

紫升

点开每个也没你,你会看到页面是五颜六色的,别担心,这些颜色都是有明确的意义:

安装依赖

$ yarn add jest -D
# babel
$ yarn add babel-jest -D
# enzyme
$ yarn add enzyme jest-enzyme enzyme-adapter-react-16 enzyme-to-json -D
# react-native-mock-render
$ yarn add react-native-mock-render -D
# types
$ yarn add @types/enzyme @types/jest @types/react @types/react-native -D

工具介绍:

配置

jest.config.js

module.exports = {
  preset: 'react-native',
  verbose: true,
  collectCoverage: true,
  moduleNameMapper: {
    // for https://github.com/facebook/jest/issues/919
    '^image![a-zA-Z0-9$_-]+$': 'GlobalImageStub',
    '^[@./a-zA-Z0-9$_-]+\\.(png|gif)$': 'RelativeImageStub',
  },
  setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
  snapshotSerializers: ['enzyme-to-json/serializer'],
};

注意:Jest 在 24.1.0 之后只能使用 setupFilesAfterEnv

jest.setup.js

import 'react-native';
import 'react-native-mock-render/mock';
import 'react-native/Libraries/Animated/src/bezier'; // for https://github.com/facebook/jest/issues/4710
import Enzyme from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';

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

enzyme 入门

enzyme 是 Airbnb 开源的 react 测试类库,提供了一套简洁强大的 API,并通过 jquery 风格的方式进行 dom 处理,开发体验十分友好. 它提供三种测试方法.

shallow

shallow 返回组件的浅渲染,对官方 shallow rendering 进行封装。浅渲染 作用就是:它仅仅会渲染至虚拟 dom,不会返回真实的 dom 节点,这个对测试性能有极大的提升。shallow 只渲染当前组件,只能能对当前组件做断言

mount

mount 方法用于将 React 组件加载为真实 DOM 节点。mount 会渲染当前组件以及所有子组件。多数情况下,shallow 方法就能满足我们的需求了。ref 测试则旨在 mount 模式下生效。

render

render 采用的是第三方库 Cheerio 的渲染,渲染结果是普通的 html 结构,对于 snapshot 使用 render 比较合适。

组件测试

组件快照测试

当我们要确保 UI 不会意外更改时,快照测试都是非常有用的工具。通过 toMatchSnapshot 即可完成。

describe('Button Component', () => {
  it('basic render', () => {
    const component = renderer.create(<Button />).toJSON();
    expect(component).toMatchSnapshot();
  });
});

生命周期测试

componentDidMount

通过调用 shallow 和 mount 方法,可以触发 componentDidMount 生命周期:

import { shallow } from 'enzyme';

function setup(props = {}) {
  const wrapper = shallow(<CarouselComponent />);
  const instance = wrapper.instance();
  return { wrapper, instance };
}

describe('Carousel Component', () => {
  it('renders correctly', () => {
    setup();
  });
});

也可以通过 wrapper.setState 方法进行触发:

import { shallow } from 'enzyme';

function setup(props = {}) {
  const wrapper = shallow(<Component {...props} />);
  const instance = wrapper.instance();
  return { wrapper, instance };
}

describe('Component', () => {
  it('renders correctly', () => {
    const { wrapper } = setup();
    wrapper.setState({
      enable: true,
    });
  });
});

componentWillUnMont

通过调用 wrapper.unmount() 可以触发 componentWillUnMont 生命周期:

import { shallow } from 'enzyme';

function setup(props = {}) {
  const wrapper = shallow(<Component />);
  const instance = wrapper.instance();
  return { wrapper, instance };
}
describe('Carousel Component', () => {
  it('renders correctly', () => {
    const { wrapper } = setup();
    expect(wrapper).toMatchSnapshot();
    wrapper.unmount();
    expect(wrapper).toMatchSnapshot();
  });
});

componentWillReceiveProps

可以通过 wrapper.setProps 方法触发:

import { shallow } from 'enzyme';

function setup(props = {}) {
  const wrapper = shallow(<Component {...props} />);
  const instance = wrapper.instance();
  return { wrapper, instance };
}

it('componentWillReceiveProps', () => {
  const { wrapper, instance } = setup({
    autoplay: true,
  });
  wrapper.setProps({ autoplay: false });
});

定时器模拟(Timer Mocks)

原生定时器功能(即 setTimeout,setInterval,clearTimeout,clearInterval)对于测试环境来说不太理想,因为它们依赖于实时时间。 Jest 可以将定时器换成允许我们自己控制时间的功能。

这里我们通过调用 jest.useFakeTimers() 来启用假定时器。然后在需要的时候执行 jest.runOnlyPendingTimers() 来触发定时器:

import { shallow } from 'enzyme';

jest.useFakeTimers();

it('autoplay methods with count(2) and os(ios)', () => {
  const { wrapper, instance } = setup({
    autoplay: true,
    loop: false,
  });
  wrapper.setState({ isScrolling: true }, () => {
    jest.runOnlyPendingTimers();
  });
});

FAQ

如何忽略某一块代码

添加以下格式的注释到要忽略的代码块前即可:

/* istanbul ignore next */

使用 mount 时,忽略 React Native 的警告

describe('mounting', () => {
    const origConsole = console.error;
    beforeEach(() => {
      console.error = () => {};
    });
    afterEach(() => {
      console.error = origConsole;
    });
    it ......
       mount....
});

常见 issues