使用 Jest 和 Enzyme 进行 React Native 单元测试|技术点评

你们好,我是 @大宁的洛竹html

本文首发于 洛竹的官方网站前端

本文已受权掘金开发者社区公众号独家使用,包括但不限于编辑、标注原创等权益。react

单元测试是什么 🧐

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

一般来讲,程序员每修改一次程序就会进行最少一次单元测试,在编写程序的过程当中先后极可能要进行屡次单元测试,以证明程序达到软件规格书要求的工做目标,没有程序错误。在 TDD 中,甚至是先根据设计编写单元测试,而后根据单元测试写代码。ios

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

其余测试

前端测试常见的测试类型有单元测试(Unit testing)、集成测试(Integration testing)、端到端(E2E testing)测试,通常咱们投入的测试资源排序以下:程序员

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

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

测试覆盖率

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

  • Statements(stmts):表达式覆盖率,是否是每一个表达式都执行了?
  • Branches(Branch):分支覆盖率,是否是每一个 if 代码块都执行了?
  • Functions(Funcs):函数覆盖率,是否是每一个函数都调用了?
  • Lines(Lines):行覆盖率,是否是每一行都执行了?

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

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

点击 App.js 能够查看单个文件的测试覆盖率状况:

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

  • 粉紅色的代码: 还没有被执行的 statement 或 function
  • 黄色的代码: 沒被覆盖到的 branch
  • I: 表明 if-else 的 if 没有被执行

  • E: 表明 if-else 的 if 没有被执行

  • Nx: 表明代码块被执行到的次数,能够做为代码性能的参考依据

安装依赖

$ 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: Jest 是一个使人愉快的 JavaScript 测试框架,专一于简洁明快。
  • enzyme: Enzyme 是用于 React 的 JavaScript 测试实用程序,能够更轻松地测试 React 组件的输出。您还能够根据给定的输出进行操做,遍历并以某种方式模拟运行时。
  • jest-enzyme: 针对 enzyme 的 Jest 断言
  • enzyme-adapter-react-16: React Native 测试所需的桥接器
  • enzyme-to-json: 将 Enzyme wrappers 转换成符合 Jest 快照测试的 JSON 格式。
  • react-native-mock-render: A fully mocked and test-friendly version of react native

配置

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'],
};
复制代码
  • collectCoverage: 生成测试覆盖率报告
  • setupFilesAfterEnv: 使用 Jest 运行安装文件以配置 Enzyme 和适配器(以下文jest.setup.js中所示),以前是setupTestFrameworkScriptFile,也可使用setupFiles
  • snapshotSerializers:推荐使用序列化程序使用enzyme-to-json,它的安装和使用很是简单,并容许您编写简洁的快照测试。

注意: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

经过调用 shallowmount 方法,能够触发 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

相关文章
相关标签/搜索