• Jest单测实践篇


    快照测试

    快照测试在你要确保你的UI没有发生改变的时候非常有用。jest的快照测试为文本测试,第一次执行时存储本次的快照,然后在之后的测试过程中进行文本比对。

    toMatchSnapshot() 方法

    import React from 'react';
    import Link from '../Link.react';
    import renderer from 'react-test-renderer';
    
    it('renders correctly', () => {
      const tree = renderer
        .create(<Link page="http://www.facebook.com">Facebook</Link>)
        .toJSON();
      expect(tree).toMatchSnapshot();
    });
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    The snapshot artifact should be committed alongside code changes, and reviewed as part of your code review process.
    注意生成的快照文件应该和代码的变更一起被提交和Review。

    如何更新快照

    有时候快照测试不通过是因为组件更新了,那么快照如何更新呢?

    (1)你可以通过jest --updateSnapshot or jest -u 方法来让jest为所有快照测试失败的组件重新生成快照。如果你想精确控制哪些快照被更新,可以用–testNamePattern参数指定正则表达式。

    (2) 使用watch模式来更新 jest --watch

    在这里插入图片描述
    按i可以交互式的更新失败的快照。

    更多请参考:
    https://jestjs.io/docs/en/snapshot-testing

    如何测试回调函数被成功调用

    测试回调函数被调用

    (1) 使用jest.fn()函数生成模拟函数

    const onDropdownVisibleChange = jest.fn();
    
    • 1

    (2) 在组件的回调中指定该模拟函数为回调函数

    const wrapper = mount(
     <Select open onDropdownVisibleChange={onDropdownVisibleChange}>
       <Option value="1">1</Option>
     </Select>,
    );
    
    • 1
    • 2
    • 3
    • 4
    • 5

    (3-1) 模拟点击后进行断言,toHaveBeenLastCalledWith断言被特定参数调用

    wrapper.find('.ant-select').simulate('click');
    expect(onDropdownVisibleChange).toHaveBeenLastCalledWith(false);
    
    • 1
    • 2

    完整实例:

    const onDropdownVisibleChange = jest.fn();
    const wrapper = mount(
      <Select open onDropdownVisibleChange={onDropdownVisibleChange}>
      <Option value="1">1</Option>
    </Select>,
    );
    wrapper.find('.ant-select').simulate('click');
    expect(onDropdownVisibleChange).toHaveBeenLastCalledWith(false);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    (3-2) 模拟点击后进行断言,toHaveBeenCalled断言被调用

    const onDropdownVisibleChange = jest.fn();
    const wrapper = mount(
      <Select open onDropdownVisibleChange={onDropdownVisibleChange}>
      <Option value="1">1</Option>
    </Select>,
    );
    wrapper.find('.ant-select').simulate('click');
    expect(onDropdownVisibleChange).toHaveBeenCalled();
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    如何测试有时间方法的组件

    jest.useFakeTimers: 模拟时间流逝

    通常一些本地的时间方法比如setTimeout等不太适合测试环境,因为这些方法会依赖真实的时间流逝。jest可以交换这些时间函数,控制时间的推移,比方说:

    beforeAll(() => {
      jest.useFakeTimers();
    });
    
    // 或者有多个测试用例使用在每个测试用例执行之前执行
    beforeEach(() => {
      jest.useFakeTimers();
    });
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    jest.runAllTimers(): 控制时间运行

    还有一些测试用例用于测试某个回调一秒后会被调用:

    test('calls the callback after 1 second', () => {
      const timerGame = require('../timerGame');
      const callback = jest.fn();
    
      timerGame(callback);
    	
    	// 这个时间点还没有被调用
      expect(callback).not.toBeCalled();
    
      // 快进,让所有时间回调都执行
      jest.runAllTimers();
    
      // 现在回调被调用
      expect(callback).toBeCalled();
      expect(callback).toHaveBeenCalledTimes(1);
    });
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    jest.useRealTimers(): 使用真实的时间

    在测试过程中可能想要恢复时间正常流逝,可以使用useRealTimers()方法恢复

    更多关于时间控制的方法关注:
    Timer Mocks · Jest

    如何模拟事件进行测试

    wrapper.simulate()方法

    wrapper.find(‘a’).simulate(‘click’);

    如:

    class Foo extends React.Component {
      constructor(props) {
        super(props);
        this.state = { count: 0 };
      }
    
      render() {
        const { count } = this.state;
        return (
          <div>
            <div className={`clicks-${count}`}>
              {count} clicks
            </div>
            <a href="url" onClick={() => { this.setState({ count: count + 1 }); }}>
              Increment
            </a>
          </div>
        );
      }
    }
    
    const wrapper = shallow(<Foo />);
    
    expect(wrapper.find('.clicks-0').length).to.equal(1);
    wrapper.find('a').simulate('click');
    expect(wrapper.find('.clicks-1').length).to.equal(1);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    模拟input框输入

    wrapper.find('.ant-input').simulate('change', { target: { value: 'test' } });
    
    • 1

    模拟checkbox change输入

    wrapper
          .find('.ant-checkbox-input')
          .at(0)
          .simulate('change', { target: { checked: true } });
    
    • 1
    • 2
    • 3
    • 4

    如何测试方法被正确的参数调用

    比方说antd的一个测试用例是检测用户如果全量引入antd的包,而不是动态引入组件的话,那么会有个提示,怎么验证这个提示被成功展示了呢?

    jest.spyOn()方法

    (1) 监控对应的方法,如这里是console.warn方法,但是不让他真的输出出来。

    const warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
    
    • 1

    (2)使用断言toBeCalledWith,被用xxx参数调用

    expect(warnSpy).toBeCalledWith(
          'You are using a whole package of antd, please use https://www.npmjs.com/package/babel-plugin-import to reduce app bundle size.',
        );
    
    • 1
    • 2
    • 3

    (3) 恢复

    warnSpy.mockRestore();
    
    • 1

    更多参考:https://jestjs.io/docs/jest-object

    如何测试组件的属性与状态是否正确

    wrapper.props() 获取当前的组件属性

    expect(dropdownWrapper.props().visible).toBe(true);
    
    • 1

    设置属性

    wrapper.setProps({ open: false });
    
    • 1

    wrapper.instance().state 获取当前对象状态

    expect(wrapper.instance().state.status).toBe(0);
    expect(wrapper.instance().state.placeholderStyle).toBe(undefined);
    
    • 1
    • 2

    如何测试debounce过的方法

    如果说组件代码中使用了debounce方法,比方说经常我们需要用debounce去控制请求的发送频率:

    import debounce from 'lodash/debounce';
    //...
    this.fetchData = debounce(this.fetchUserData, this.props.delayTime);
    
    • 1
    • 2
    • 3

    但是在测试用例中怎么去让debounce返回的函数被立即执行呢?毕竟原函数默认是需要delay一段时间后再执行的,答案是可以使用jest的mock方法。

    测试步骤

    1. 在测试用例中引入debounce

    import debounce from 'lodash/debounce';
    
    • 1

    2. 使用jest.mock运行这个函数,给他加上mock方法

    jest.mock('lodash/debounce');
    
    • 1

    这样 debounce 方法中就会被加上 mockImplementation 以及 mockRestore 方法

    3. 在运行测试用例前mock该函数实现,运行测试用例后restore

    beforeEach(() => {
      jest.useFakeTimers();
      debounce.mockImplementation(fn => fn);
    });
    
    afterEach(() => {
      jest.useRealTimers();
      debounce.mockRestore();
    });
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    比方说在 beforeEach 和 afterEach 中分别调用 mockImplementation 和 mockRestore , fn => fn 的含义是指返回这个函数本身,比方说:

    this.fetchData = debounce(this.fetchUserData, this.props.delayTime);
    
    • 1

    运行完debounce函数后, this.fetchData === this.fetchUserData ,那么调用 this.fetchData 函数的时候请求就会立即执行了。

    测试用例案例

    一个完整的测试用例的案例参考如下:

    import * as React from 'react';
    // eslint-disable-next-line import/no-extraneous-dependencies
    import { mount } from 'enzyme';
    import debounce from 'lodash/debounce';
    import UserSearch from '../index';
    
    jest.mock('lodash/debounce');
    
    describe('', () => {
      beforeEach(() => {
        jest.useFakeTimers();
        debounce.mockImplementation(fn => fn);
      });
    
      afterEach(() => {
        jest.useRealTimers();
        debounce.mockRestore();
      });
    
      it('should stop fetching if users empty', done => {
        const wrapper = mount(
          <UserSearch userSearchRequest={() => Promise.resolve([])} />,
        );
    
        wrapper.find('input').simulate('change', { target: { value: '1' } });
        jest.runAllTimers();
    
        process.nextTick(() => {
          wrapper.update();
          expect(wrapper.instance().state.fetching).toBe(false);
          done();
        });
      });
    
    });
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35

    如何测试返回promise的方法

    异步方法通常都具有不确定性,promise的方法也是,但是我们希望在函数调用后立即能够看到组件的执行效果,怎么办呢?答案是使用 process.nextTick() 方法。

    比方说我的promise方法是这样:

    fetchData = query => {
        const { userSearchRequest } = this.props;
        userSearchRequest &&
          userSearchRequest({ query })
            .then(users => {
             	...
              dataSource = ...;
              this._mounted && this.setState({ dataSource, fetching: false });
            })
            .catch(err => {
              this._mounted && this.setState({ fetching: false });
            });
      };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    在promise完成之后setState,我希望去测试setState之后的效果,相应的测试用例如下:

    it('should stop fetching if users empty', done => {
        const wrapper = mount(
          <UserSearch userSearchRequest={() => Promise.resolve([])} />,
        );
    
        wrapper.find('input').simulate('change', { target: { value: '1' } });
        jest.runAllTimers();
    
        process.nextTick(() => {
          wrapper.update();
          expect(wrapper.instance().state.fetching).toBe(false);
          done();
        });
      });
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    nextTick 会在目前队列中的所有事件完成之后执行,保证了promise方法此时已经完成执行。

    如何测试moment相关方法

    在我们的测试当中涉及日期组件的时候,进行snapshot测试会出现snapshot总是不通过的问题,那是因为moment总是获取当前的日期,造成snapshot的变化,为了解决这个问题,我们可以mock日期,让他返回固定的日期。

    mockdate

    我们使用 mockdate 来帮我们mock 日期相关的方法

    tnpm install --save-dev mockdate
    
    • 1

    然后创建mock方法:

    import moment from 'moment';
    import MockDate from 'mockdate';
    
    export function setMockDate(dateString = '2017-09-18T03:30:07.795') {
      MockDate.set(moment(dateString));
    }
    
    export function resetMockDate() {
      MockDate.reset();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    最后在测试开始之前调用mock方法即可:

    import React from 'react';
    import { mount } from 'enzyme';
    import toJson from 'enzyme-to-json';
    import DateRanger from '..';
    import { setMockDate, resetMockDate } from './utils';
    
    describe('DateRanger', () => {
      beforeEach(() => {
        setMockDate();
      });
    
      afterEach(() => {
        resetMockDate();
      });
    
      it('should match snapshot', () => {
        const wrapper = mount(<DateRanger />);
        // 快照一致
        expect(toJson(wrapper)).toMatchSnapshot();
        wrapper.unmount();
      });
    });
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    一些组件测试实例的收集测试

    某个节点的文案正确:

    expect(
          dropdownWrapper
            .find('MenuItem')
            .at(0)
            .text(),
        ).toBe('No Data');
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    测试各个标签属性设置正确

    // 获取DOM节点
    const input = wrapper.find('.ant-alert').getDOMNode();
    // 2. getAttribute获取属性
    expect(input.getAttribute('data-test')).toBe('test-id');
    
    • 1
    • 2
    • 3
    • 4

    测试被点开的Select按钮,找到展开的dropdown组件并点击或输入

    it('should have default notFoundContent', () => {
        const wrapper = mount(<Select mode="multiple" />);
        wrapper.find('.ant-select').simulate('click');
        jest.runAllTimers();
        const dropdownWrapper = mount(
          wrapper
            .find('Trigger')
            .instance()
            .getComponent(),
        );
        expect(dropdownWrapper.find('MenuItem').length).toBe(1);
        expect(
          dropdownWrapper
            .find('MenuItem')
            .at(0)
            .text(),
        ).toBe('No Data');
      });
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    测试错误

    it('should throw error when option value is missing', () => {
        try {
          mount(
            <CheckCard.Group options={[{ value: 'Apple' }, { value: 'Pear' }, { title: 'Orange' }]} />,
          ).unmount();
        } catch (e) {
          expect(e).toBeDefined();
        }
      });
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    组件测试用例的设计

    前言

    React组件的测试和nodejs的单元测试有很大不同,我在对组件进行测试的时候,经常迷惑的一个点是我应该测试些什么?下面是在实战过程中总结的一些经验。

    测试目标

    首先要确定自己的测试目标,比较直接的测试目标是
    ● 测试覆盖率,一般需要达到95%以上是一个比较好的状态,包括
    ○ 行覆盖率
    ○ 分支覆盖率
    ● 功能覆盖,有时候覆盖率达到了,不过很多不同使用场景仍然需要测试,比方说各种props是否正常,可以结合组件的demo,设计文件去设计测试用例

  • 相关阅读:
    SpringBoot整合MongoDB 并进行增删改查
    计算机网络-第4章 网络层
    Vue3从入门到精通(三)
    Golang接口
    JavaScript从入门到精通系列第三十一篇:详解JavaScript中的字符串和正则表达式相关的方法
    『Java安全』XStream 1.4.13反序列化漏洞CVE-2020-26217复现与浅析
    CSS详细基础(五)选择器的优先级
    4-9封装与隐藏
    P2432 zxbsmk爱查错,字符串线性dp
    【售货系统的Web测试】
  • 原文地址:https://blog.csdn.net/weixin_43606158/article/details/123886866