组件测试

组件测试本身就是单元测试,但是其又包括快照测试(Snapshot Test)这种特有的测试。

组件测试基础概念

快照测试

一般的快照测试是测试你的渲染组件的图片,并将其与组件的以前的图片进行比较。Jest 的快照测试也是其特有的功能,它不是拍摄渲染组件的图片,而是渲染组件 标记 的快照。 这使得 Jest 快照测试比传统快照测试快得多。

如何生成快照?

Jest 首次运行快照测试,会让 UI 框架生产一个可读的快照,再次测试时便会通过比对快照文件和新 UI 框架产生的快照判断测试是否通过。对于 React ,我们可以通过下面的方法生产一个快照:

import React from 'react';
import Link from '../Link.react';
import renderer from 'react-test-renderer';

it('should renders correctly', () => {
    const tree = renderer.create(
        <Link page="http://www.facebook.com">Facebook</Link>
    ).toJSON();
    expect(tree).toMatchSnapshot();
});

快照长什么样?

运行快照测试的时候将会自动生成一份以 .snap 结尾的文件,一般在该组件所在文件夹下的 snapshots 文件夹。

exports[`should renders correctly 1`] = `
<a
    className="normal"
    href="http://www.facebook.com"
    onMouseEnter={[Function]}
    onMouseLeave={[Function]}
>
    Facebook
</a>
`;

如果测试用例中调用两次快照方法 .toMatchSnapshot() 那么会生成两个快照:

exports[`should renders correctly 1`] = `
<a
    className="normal"
    href="http://www.facebook.com"
    onMouseEnter={[Function]}
    onMouseLeave={[Function]}
>
    Facebook
</a>
`;

exports[`should renders correctly 2`] = `
<a
    className="normal"
    href="http://www.facebook.com"
    onMouseEnter={[Function]}
    onMouseLeave={[Function]}
>
    Facebook
</a>
`;

这个可读的快照文件以可读的形式展示了 React 渲染出的 DOM 结构。相比于肉眼观察效果的 UI 测试,快照测试直接由Jest进行比对、速度更快;而且由于直接展示了 DOM 结构,也能让我们在检查快照的时候,快速、准确地发现问题。

如何更新快照?

详细参考:https://jestjs.io/docs/zh-Hans/snapshot-testing

浅渲染

浅渲染(shallow rendering)意思就是只渲染组件中的第一层,这样测试执行器就不需要关心 DOM 和执行环境了。

在实际开发中,组件的层级非常深,所以测试顶层组件时,如果需要把所有子组件全部渲染出来,成本变得非常高。因为 React 组件良好的封装性,测试组件时,大部分测试只需要关注组件本身,它的子组件测试应该在子组件的测试代码里做。这样测试执行得更快。

但浅渲染的优点也对于着其缺点:只能测试一级节点,如果需要测试子组件,那就只能做全渲染。

例如有如下组件:

const ButtonWithIcon = ({icon, children}) => (
    <button><Icon icon={icon} />{children}</button>
);

React 渲染后:

<button>
    <i class="icon icon_coffee"></i>
    Hello Jest!
</button>

但是如果经过 shallow rendering 将会是这样:

<button>
    <Icon icon="coffee" />
    Hello Jest!
</button>

注意 Icon 组件并没有被渲染。

全渲染

全渲染(full rendering)就是完整渲染出当前组件及其所有子组件,就像在真实浏览器渲染那样,当组件内部直接改变了 DOM 时,就需要使用全渲染来测试。全渲染需要真实地模拟 DOM 环境,流行的做法有以下几种:

  • 使用 JSDOM:使用 JS 模拟 DOM 环境,能满足90%的使用场景。这是 Jest 内部所使用的全渲染框架。

  • 使用 Cheerio:类似 JSDOM,更轻的实现,类似 jQuery 的语法。这是 Enzyme 内部使用的全渲染框架。

  • 使用 Karma:在真实的浏览器中执行测试,也支持在多个浏览器中依次执行测试,使用的是真实DOM 环境,但速度稍慢。

组件测试框架

Jest 组件测试

Jest 在组件测试方面特点有:

  • 特有的快照测试功能:通过比对 UI 代码生成的快照文件,确保组件呈现正确的样式。

  • JSDOM:不需要真实 DOM 环境执行,而是 JSDOM 模拟的 DOM。

以上的例子如果用 Jest 来写,如下:

import React from 'react';
import renderer from 'react-test-renderer';
import ButtonWithIcon from './ButtonWithIcon';

it('should render ButtonWithIcon correctly', () => {
  const component = renderer.create(ButtonWithIcon({'coffee', 'Hello Jest!'}));
  expect(result.type).toBe('button');
  expect(result.props.children).toEqual([
    <Icon icon="coffee" />,
    'Hello Jest!'
  ])
})

Enzyme 组件测试

Enzyme 是由 Airbnb 开源的 React 组件测试框架,与 Jest 的组件测试相比,Enzyme 提供类似 jQuery 操作 DOM 的语法,在做测试断言时更灵活、易用。

Enzyme 提供 3 种不同的方式来测试组件:

  • shallow:推荐的方式,浅渲染,只会渲染本地组件内容(只渲染不包含 children 的组件),引用的外部组件不会渲染,提供更好的隔离性。

  • render:如果 shallow 不能满足,才会使用它,能够渲染所有的子组件。基于 Cheerio 来模拟 DOM 环境(Cheerio 是类似 JSDOM 的另一框架)。

  • mount:类似 render,会做全渲染,对测试生命周期非常有用,能够访问到组件的生命周期方法,比如 componentDidUpdate 等。一般用于集成测试。

Enzyme Selector

编写组件测试

测试 rendering

对于大部分非交互组件使用 toMatchSnapshot() 即可:

import { shallow } from 'enzyme';

it('should render a label', () => {
    const wrapper = shallow(
        <Label>Hello Jest!</Label>
    );
    expect(wrapper).toMatchSnapshot();
});

it('should render a small label', () => {
    const wrapper = shallow(
        <Label small>Hello Jest!</Label>
    );
    expect(wrapper).toMatchSnapshot();
});

it('should render a grayish label', () => {
    const wrapper = shallow(
        <Label light>Hello Jest!</Label>
    );
    expect(wrapper).toMatchSnapshot();
});

写组件测试的时候需要将 Enzyme 和 Jest 结合起来使用,两者是互补的,它们已经是 React 应用测试中大家公认的标准库。

测试 props

有时候你想更有针对性地测试,比如组件的某个属性是否渲染正确,你可以使用 Enzyme API + Jest 断言的组合方式:

it('should render a document title', () => {
    const props = { title: 'Events' }
    const wrapper = shallow(<DocumentTitle {...props} />);
    expect(wrapper.prop('title')).toEqual('Events');
});

it('should render a document title and a parent title', () => {
    const props = { title: 'Events', parent: 'Event Radar' }
    const wrapper = shallow(<DocumentTitle {...props} />);
    expect(wrapper.prop('title')).toEqual('Events — Event Radar');
});

在某些情况下是不太适合使用 toMatchSnapshot() 的,例如组件中 id 属性是随机产生的:

it('should render a popover with a random ID', () => {
    const wrapper = shallow(
        <Popover>Hello Jest!</Popover>
    );
    expect(wrapper.prop('id')).toMatch(/Popover\d+/);
});

如果用 toMatchSnapshot() 的话每次测试都会不过,需要不断更新快照。

测试 events

Enzyme 可以模拟事件操作后(例如 click 或 change等)再和测试快照进行对比:

it('should render Markdown in preview mode when click toggle-preview', () => {
    const wrapper = shallow(
        <MarkdownEditor value="**Hello** Jest!" />
    );
    expect(wrapper).toMatchSnapshot();
    wrapper.find('[name="toggle-preview"]').simulate('click');
    expect(wrapper).toMatchSnapshot();
});

如果你需要测试与组件中某一个元素或子组件的交互,此时需要调用 Enzyme 中的 render/mount 方法而不是 shallow 方法:

it('should open a code editor after clicked the button', () => {
    const wrapper = render(
        <Playground code={code} />
    );
    expect(wrapper.find('.ReactCodeMirror')).toHaveLength(0);
    wrapper.find('button').simulate('click');
    expect(wrapper.find('.ReactCodeMirror')).toHaveLength(1);
});

测试 event handlers

和 Testing events 差不多,主要区别是 Testing events 关注的是事件后对组件渲染产生的影响,而 Testing event handlers 关注的是事件处理函数:

it('should pass a selected value to the onChange handler', () => {
    const value = '2';
    const onChange = jest.fn();
    const wrapper = shallow(
        <Select items={ITEMS} onChange={onChange} />
    );
    expect(wrapper).toMatchSnapshot();
    wrapper.find('select').simulate('change', {
        target: { value },
    });
    expect(onChange).toBeCalledWith(value);
});

Last updated