Jest+Enzyme前端单元测试进阶

695 阅读6分钟

Jest的快照测试snapshot

每当你想要确保你的UI不会有意外的改变,快照测试是非常有用的工具。

// app.js
export function app() {
    return {
        server: 'http://localhost',
        port: '8080',
    }
}

// 测试文件
test('app快照测试', () => {
    expect(app()).toMatchSnapshot();
})

第一次运行此测试案例后,会在同一目录下生成一个__snapshots__的文件夹,里面是app.js文件的快照文件,文件名app.test.js.snap

exports[`app快照测试 1`] = `
Object {
  "server": "http://localhost",
  "port": "8080",
}
`;

之后运行此测试案例,生成的快照会和之前生成的快照做比对,如果快照一致,则测试通过;如果更改了代码,快照不一致,则测试不通过,此时在终端中按u键即可更新快照,然后测试就会通过了,快照会保存为最新的快照。

如果我们一次性修改了多个快照,那么多个快照测试不会通过,此时按u键,则所有的快照都会更新。但是如果我们想一个个的去检查快照,决定是否更新时,我们需要一个个的更新快照。此时我们按w键,会出现多种快捷键模式,而i键交互式的一个个的更新快照符合我们的需求。

i键进入交互模式,会提示一个快照测试不通过,是否需要更新,按u键更新快照。按s

然后该快照会被更新,并且进入下一个不通过的快照测试,依次更新快照直到所有需要更新的快照全部通过后,按enter键退出i模式到正常测试模式。

如果我们的快照测试中有时间{time: new Date()},每次快照测试时,新快照的time值与老快照的值都不一致,会导致我们的快照测试不通过,那么此时测试案例需要做出修改。

// app.js
export function app() {
    return {
        server: 'http://localhost',
        port: '8080',
        time: new Date(),
    }
}

// 测试文件
test('app快照测试', () => {
    expect(app()).toMatchSnapshot({
        time: expect.any(Date)), // 表明time可以是任意时间,Date、String、Number
    });
})

如果是要给组件快照测试,推荐react-test-renderer,可以更好的帮助我们对组件进行快照测试。

npm i react-test-renderer -D
// 测试文件
import renderer from 'react-test-renderer';
test('快照测试', ()=> {
    const tree = renderer.create(<App />);
    expect(tree.toJSON()).toMatchSnapshot();
})

mock异步函数

之前我们讲了可以通过mock axios模块,来实现异步请求的测试,现在我们通过mock异步请求函数来实现异步请求测试。

一个发异步请求的文件

// api.js
export const fetchData = () => {
    return axios.get('http://www.dell-lee.com/react/api/demo.json');
}

在与它同级的目录下创建一个文件夹__mocks__,下面再创建一个同样的文件api.js

// __mocks__/api.js
export const fetchData = () => {
    return new Promise((resolved, reject) => {
        resolved({
            data: 123,
        })
    })
}

这样在测试文件中,jest会自动到__mocks__文件夹下面去找模拟的api.js文件进行测试:

import { fetchData } from './api.js';
jest.mock('./api.js');

test('测试返回值为123', () => {
    return fetchData().then(res => {
        expect(res.data).toBe(123);
    })
})

比如,有时候在api.js文件中有不需要mock的函数getNumber(),此时在测试文件中引入此函数。使用jest.requireActual()这个方法,会从真正的api.js文件中取引入函数。

const { getNumber } = jest.requireActual('./api.js');

mock timers

创建一个timer.js文件,3秒后执行callback函数:

// timer.js 
export default (callback) => {
    setTimeout(() => {
        callback();
    }, 3000)
}

// 测试案例
test('测试回调函数', (done) => {
    timer(() => {
        expect(1).toBe(1);
        done();
    })
})

测试案例会等3秒后才返回测试结果,如果等待时间太久,是不利于我们做测试的,所以需要添加jest.useFakeTimers(),来帮助我们模拟定时器,用jest.runAllTimers()来快进时间,不必再等待时长。

// 测试案例
jest.useFakeTimers();
test('测试回调函数', () => {
    const fn = jest.fn(); // 使用jest来mock一个回调函数
    timer(fn);
    jest.runAllTimers(); 
    expect(fn).toHaveBeenCalledTimes(1);
})
  • jest.runAllTimers():快进时间,使所有定时器回调被立刻执行
  • jest.runonlyPendingTimers():只立即执行在等待中的定时器
  • jest.advanceTimersByTime(1000):快进时间至1秒,执行定时器回调
// timer.js
export default (callback) => {
    setTimeout(() => {
        callback();
        setTimeout(() => {
            callback();
        }, 3000)
    }, 3000)
}

// 测试案例
jest.useFakeTimers();
test('测试回调函数', () => {
    const fn = jest.fn();
    timer(fn);
    jest.runAllTimers(); 
    expect(fn).toHaveBeenCalledTimes(2);
})

test('测试回调函数', () => {
    const fn = jest.fn();
    timer(fn);
    jest.runonlyPendingTimers(); 
    expect(fn).toHaveBeenCalledTimes(1);
})

test('测试回调函数', () => {
    const fn = jest.fn();
    timer(fn);
    jest.advanceTimersByTime(3000); 
    expect(fn).toHaveBeenCalledTimes(1);
    jest.advanceTimersByTime(3000); 
    expect(fn).toHaveBeenCalledTimes(2);
})

Enzyme的配置及使用

Enzyme是Airbnb开源的React测试工具库库,它功能过对官方的测试工具库ReactTestUtils的二次封装,提供了一套简洁强大的 API,并内置Cheerio,实现了jQuery风格的方式进行DOM 处理,开发体验十分友好。在开源社区有超高人气,同时也获得了React 官方的推荐。

安装enzyme,在开发环境

npm i --save-dev enzyme enzyme-adapter-react-16

使用enzyme,在测试文件中引入enzyme

import Enzyme from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';

Enzyme.configure({ adapter: new Adapter() });
test('', () => {
    ....
})

enzyme提供了3种测试方法,每个测试方法包含多个API

  • shallow:浅渲染,是对官方的Shallow Renderer的封装。将组件渲染成虚拟DOM对象,只会渲染第一层,子组件将不会被渲染出来,使得效率非常高。不需要DOM环境, 并可以使用jQuery的方式访问组件的信息。
  • mount:完全渲染,它将组件渲染加载成一个真实的DOM节点,用来测试DOM API的交互和组件的生命周期。用到了jsdom来模拟浏览器环境。
  • render:静态渲染,它将React组件渲染成静态的HTML字符串,然后使用Cheerio这个库解析这段字符串,并返回一个Cheerio的实例对象,可以用来分析组件的html结构。 三种方法中,shallowmount因为返回的是DOM对象,可以用simulate进行交互模拟,而render方法不可以。一般shallow方法就可以满足需求,如果需要对子组件进行判断,需要使用render,如果需要测试组件的生命周期,需要使用mount方法。

jest+enzyme的使用

// App.js
import React from 'react';
function App(){
    return (
        <div className='app' title='btn'>hello world</div>
    )
}
export default App;
// 测试文件
import App from './App.js';
import Enzyme, { shallow, mount, render } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
Enzyme.configure({ adapter: new Adapter() });

test('测试节点', () => {
    const wrapper = shallow(<App />);
    expect(wrapper.find('.app').length).toBe(1);
    expect(wrapper.find('.app').prop('title')).toBe('btn');
})

GitHubenzyme中有关于jest-enzyme的一些新的匹配器,可以更为方便的使用。

安装jest-enzyme:

npm i jest-enzyme --save-dev
// package.json
"jest": {
  "setupFilesAfterEnv": ['./node_modules/jest-enzyme/lib/index.js'],
}

那么我们便可以使用新增的匹配器来重新测试上面的案例:

test('测试节点', () => {
    const wrapper = shallow(<App />); 
    expect(wrapper.find('.app')).toExist();
    expect(wrapper.find('.app')).toHaveProp('title', 'btn');
})

jest-enzyme中新增的匹配器:

Enzyme的一些API:

  • type():返回当前组件的类型
  • text():返回当前组件的文本内容
  • html():返回当前组件的HTML代码形式
  • props():返回根组件的所有属性
  • prop(key):返回根组件的指定属性
  • state([key]):返回根组件的状态
  • setState(nextState):设置根组件的状态
  • setProps(nextProps):设置根组件的属性
  • find(selector):返回元素
  • debug():返回当前组件的HTML代码的字符串形式
  • exists([selector]):返回当前组件是否存在的布尔值
  • simulate(event[, ...args]:组件交互
test('测试input change事件', () => {
    const wrapper = shallow(<App />);
    const inputElem = wrapper.find('input');
    inputElem.simulate('change', {
        target: { value: 'hello world' },
    })
    expect(inputElem.prop('value')).toBe('hello world');
})