用Jest编写单元测试

Jest

到新公司的第一个任务是写个公共的Calendar组件,还挺有意思的。写完之后,作为公共组件,当然要有单测,单侧覆盖率也是越高越好。充分的单测能够降低后续迭代造成bug的几率。

所以来简单记录下使用jest的过程,文本主要讲大致用法,具体细节还是看文档

目录结构

----- Component  // 你的组件目录
	index.tsx  // 当前组件源码
	...
	---- __test__ // 单测相关目录
		---- index.test.jsx // 单测编写
		---- __snapshots__ // 快照目录
			---- index.test.tsx.snap // 快照	

关于快照 snapshot

snapshot用于确保组件的UI不会发生意外的更改。snapshot test 用来确保组件有被正确的渲染。通常是先渲染组件,然后用渲染结果跟之前生成的快照文件惊醒比较:

  • 如果不匹配,则测试不通过
  • 如果主动更新了组件,则要更新快照

简单用例

// 首先引入要测试的组件及其依赖
import React from 'react';
import dayjs from 'dayjs';
import { mount } from 'enzyme';
import { act } from 'react-dom/test-utils';
import MockDate from 'mockdate';
import toJson from 'enzyme-to-json';
import Calendar from '../index';
import styles from '../Calendar.styl';
import { throttle } from '../util';
import { CalendarPropsRef } from '../types';

// mock today
// 这里的作用是使用mockdate包,将后边的所有 new Date()的值改为 new Date('2022-05-02')
MockDate.set(new Date('2022-05-02'));

// 为什么要 MockDate?
// 这里是一个很有意思的例子,由于测试的是一个Calendar日历组件,而日历要渲染的日期是跟当前时间相关的
// 即:默认情况下,Calendar会渲染今天所在的月份,同时对于今天,Calendar会有特殊的样式
// 这就会导致,我们的snapshot渲染出的UI,每次都是不一样的,在不同的日期跑单测,snapshot必然比匹配
// 所以,这里通过mockData,把每次测试的 今天 固定在 2022.5.2这一天
// 这样就保证了snapshot不因日期变化而变化,解决了问题

// 测异步用
const delay = (ms = 100) => new Promise((resolve) => setTimeout(resolve, ms));

// describe(name, fn) 用来创建一个block,将多个相关联的test组合在一起
// 一般来说一个组件的所有单测组成一个 describe
describe('Calendar', () => {
  // 这里的it其实是test的别名,语义上来说,it should be ... 来表示一个test
  // 所以下边写成 test('render correct', () => { 也一样
  // it(name, fn, timeout) 分别是 test名称,fn表示测试过程,timeout表示在test终止之前等待多久
  // 如果一个test返回了一个Promise,那么test会等Promise resolve之后,才算test完成

  // 下边这个是一个典型的 snapshot test
  // 用来确保组件UI被正确渲染
  it('render correct', () => {
    // 渲染一个Calendar组件
    const wrapper = mount(<Calendar />);
    // 渲染结果toJson之后,跟之前生成的快照文件匹配
    expect(toJson(wrapper)).toMatchSnapshot();
  });

  // 测试Calendar的 className prop 是否生效
  it('custom class', () => {
    // 渲染代有自定义class的组件
    const wrapper = mount(<Calendar className="test" />);
    // 看渲染结果中是否存在
    wrapper.hasClass('test');
  });

  // 测试 Calendar组件受控的情况
  // 点击日期,从2022.5.1变为2022.5.2
  it('isControlled', () => {
    // mock一个onChange方法
    const onChange = jest.fn();
    // 渲染受控的 Calendar组件,包括 value和onChange,初始为2022.5.1
    const wrapper = mount(<Calendar value={new Date('2022-05-01')} onChange={onChange} />);
    // 通过dom找到 2022.5.2的那天,然后模拟点击事件
    wrapper.find(`.${styles.calendarBodyCell}`).at(1).children().at(7).simulate('click');
    // 期望 onChange能被调用,并且入参是2022.5.2
    expect(onChange).toBeCalledWith(dayjs('2022-05-02').toDate());
  });

  // 测试ref返回的selectDay方法
  it('ref selectDay', () => {
    // 渲染Calendar,并且用ref暴露的方法
    const ref = React.createRef<CalendarPropsRef>();
    mount(<Calendar ref={ref} />);
    // selectDay 方法会引起组件状态和ui的变化
    // 对于这种操作要包装再 act内部,用来保证后续代码执行的时候,这些变化已经全部完成了
    act(() => {
      // 选择 2022.5.2这一天
      ref.current?.selectDay(new Date('2022-05-02'));
    });
    // 选完后获取值,判断是否正确
    const val = ref.current?.getValue().toDateString();
    expect(val).toBe(new Date('2022-05-02').toDateString());
  });

  // 测试一个throttle方法
  it('throttle', async () => {
    // 这里手动实现的throttle中使用了 new Date()
    // 为了不影响这个测试用例,这里把 new Date() 的行为重置回去
    MockDate.reset();
    // 写个计数器函数,每次+1
    let counter = 0;
    const fn = () => counter++;
    // 25ms 节流
    const fnT = throttle(fn, 25);
    // 连续执行其实只执行了一次,只有delay(30)毫秒之后再执行,才会再次生效
    // 1
    fnT();
    fnT();
    fnT();
    await delay(30);
    // 2
    fnT();
    fnT();
    fnT();
    await delay(30);
    // 3
    fnT();
    // 所以最后counter === 3
    expect(counter).toBe(3);
  });
});


版权声明:本文为Creabine原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。