利用 Jest 为 React 组件编写单元测试

前情回顾

react-test-renderer

react-dom/test-utils

Enzyme

Q&A

今天节目的主要内容有

前情回顾

谁还记得两周前我们说过啥?

什么是单元测试?

单元测试的基本构成

Jest 的基本使用

React 组件的单元测试有何不同呢?

没有什么不同

套路都是一样的

我们只需要根据 React 组件自身的特点测试相应功能即可

React 组件 render 的结果是一个组件数,最终会被 React 解析成纯粹由 HTML 构成的 DOM 树

React 组件可以有 state,且 state 的变化会影响 render 的结果

对应 React 组件来说,props 就是它的入参

React 组件可以拥有生命周期函数,并会在特定的时间点执行这些函数

如何以一种有利于测试的方式 render 组件?

什么是 renderer?

shallow render

vs

full render

shallow render

import ShallowRenderer from 'react-test-renderer/shallow';

const renderer = new ShallowRenderer();
  • render() 用于 render 一个组件。你可以把 ShallowRenderer 的实例想象成一个容纳被 render 组件的“空间”。
  • getRenderOutput() 在 render 之后,可以通过此命令获取 render 的结果。

Demo

const Link = ({to, children}) => (
  <a className="my-link" href={to} target="_blank" rel="noopener noreferrer">{children}</a>
);

const Header = () => (
  <div>
    <span className="brand">Hello world</span>
    <Link to="https://jd.com">JD</Link>
    <Link to="http://butler.jd.com">Butler</Link>
    <Link to="http://lrc.jd.com">lrc</Link>
  </div>
);
describe('Header', () => {
  it('should render a top level div', () => {
    const renderer = new ShallowRenderer();
    renderer.render(<Header />);
    const result = renderer.getRenderOutput();
    expect(result.type).toBe('div');
  });

  it('should render 3 Link', () => {
    const renderer = new ShallowRenderer();
    renderer.render(<Header />);
    const result = renderer.getRenderOutput();
    const childrenLink = result.props.children.filter(c => c.type === Link);
    expect(childrenLink.length).toBe(3);
  });
});

full render

import TestRenderer from 'react-test-renderer';

TestRender.create(...)

TestRenderer 实例上的方法与属性

  • .toJSON()
  • .toTree()
  • .update(element)
  • .umount()
  • .getInstance()
  • .root

test instance 上的属性与方法

.find() & findAll()

.findByType() & .findAllByType()

.findByProps() & .findAllByProps()

.instance

describe('Header', () => {
  it('should render 3 a tag with className "my-link"', () => {
    const testRenderer = TestRenderer.create(<Header />);
    const testInstance = testRenderer.root;
    expect(
        testInstance.findAll(
            node => node.type === 'a' && node.props.className === 'my-link'
        )
    ).toHaveLength(3);
  });
});
import ReactTestUtils from 'react-dom/test-utils';
  • .Simulate.{evnentName}()
  • .renderIntoDocument()
  • .scryRenderedDOMComponentsWithClass()
  • .findRenderedDOMComponentWithClass()
  • .scryRenderedDOMComponentsWithTag()
  • .findRenderedDOMComponentWithTag()

ReactTestUtils 上常用的方法

class Button extends React.Component {
  constructor() {
    super();

    this.state = { disabled: false };
    this.handClick = this.handClick.bind(this);
  }

  handClick() {
    if (this.state.disabled) { return }
    if (this.props.onClick) { this.props.onClick() }
    this.setState({ disabled: true });
    setTimeout(() => {this.setState({ disabled: false })}, 200);
  }

  render() {
    return (
      <button className="my-button" onClick={this.handClick}>{this.props.children}</button>
    );
  }
};
it('should call onClick callback if provided', () => {
  const onClickMock = jest.fn();
  const testInstance = ReactTestUtils.renderIntoDocument(
    <Button onClick={onClickMock}>hello</Button>
  );
  const buttonDom = ReactTestUtils.findRenderedDOMComponentWithClass(testInstance, 'my-button');
  ReactTestUtils.Simulate.click(buttonDom);
  expect(onClickMock).toHaveBeenCalled();
});
it('should be throttled to 200ms', () => {
  const testInstance = ReactTestUtils.renderIntoDocument(<Button>hello</Button>);
  const buttonDom = ReactTestUtils.findRenderedDOMComponentWithClass(testInstance, 'my-button');
  ReactTestUtils.Simulate.click(buttonDom);
  expect(testInstance.state.disabled).toBeTruthy();
  jest.advanceTimersByTime(199);
  expect(testInstance.state.disabled).toBeTruthy();
  jest.advanceTimersByTime(1);
  expect(testInstance.state.disabled).toBeFalsy();
});
describe('Button', () => {
  it('should be throttled to 200ms', () => {
    const wrapper = mount(<Button>hello</Button>);
    wrapper.find('.my-button').simulate('click');
    expect(wrapper.state('disabled')).toBeTruthy();
    jest.advanceTimersByTime(199);
    expect(wrapper.state('disabled')).toBeTruthy();
    jest.advanceTimersByTime(1);
    expect(wrapper.state('disabled')).toBeFalsy();
  });

  it('should call onClick callback if provided', () => {
    const onClickMock = jest.fn();
    const wrapper = mount(<Button onClick={onClickMock}>hello</Button>);
    wrapper.find('.my-button').simulate('click');
    expect(onClickMock).toHaveBeenCalled();
  });
});

Q & A

利用 Jest 为 React 组件编写单元测试

By loveky

利用 Jest 为 React 组件编写单元测试

  • 1,424