前言
- Jest介绍
- Jest配置
- Jest例子
一. Jest介绍
Jest 是 Facebook 出品的一个测试框架,相对其他测试框架, 其一大特点就是就是内置了常用的测试工具,比如自带测试执行器、断言、mock、snapshot和测试覆盖率工具,实现了开箱即用。
1.1 Jest特点:
- 易用性:基于Jasmine,提供断言库,支持多种测试风格
- 适应性:Jest是模块化、可扩展和可配置的
- 沙箱:Jest内置了JSDOM,能够模拟浏览器环境,并且并行执行
- 快照测试:Jest能够对React组件树进行序列化,生成对应的字符串快照,通过比较字符串提供高性能的UI检测
- Mock系统:Jest实现了一个强大的Mock系统,支持自动和手动mock
- 支持异步代码测试:支持Promise和async/await
- 自动生成静态分析结果:内置Istanbul,测试代码覆盖率,并生成对应的报告
1.2 Jest配置
jest.config.js配置
const path = require('path');
module.exports = {
// 测试环境,这个字段可以选择node或者是jsdom两个选项
testEnvironment: 'jsdom',
// 显示测试用例和时间
verbose: true,
// 测试文件的类型 告诉 Jest 需要处理的文件后缀
moduleFileExtensions: [
'js',
'jsx'
],
// 根目录
rootDir: path.resolve(__dirname, './'),
// 忽略文件夹
testPathIgnorePatterns: ['/node_modules/'],
// 匹配的文件 需要执行哪些目录下的测试用例
testMatch: [
'<rootDir>/test/**/*.test.js',
],
// 支持源代码中相同的 `@` -> `src` 别名
moduleNameMapper: {
"^@/(.*)$": "<rootDir>/$1"
},
// 是否开启代码测试覆盖率【建议开启】
collectCoverage: false,
// 列出包含reporter名字的列表,而Jest会用他们来生成覆盖报告
coverageReporters: ["html", "text-summary"],
// 覆盖率报告输出地址
coverageDirectory: '<rootDir>/test/coverage',
// 添加 collectCoverageFrom 数组来定义需要收集测试覆盖率信息的文件
// 测试报告想要覆盖那些文件,前面加!是避开这些文件
collectCoverageFrom: [
'src/components/**/*.{js, jsx}',
'test/**/*.{js, jsx}',
'!src/main.js',
'!**/node_modules/**'
]
};
二. Jest例子
2.1 describe、test与expect
describe 称为"测试套件或测试分组"(test suite)
test (也可以写成 it)称为"测试用例"(test case)
expect 就是判断源码的实际执行结果与预期结果是否一致,如果不一致就抛出一个错误.
测试文件中应包括一个或多个describe, 每个describe中可以有一个或多个it, 每个it中可以有一个或多个expect.
const my = {
name : "james"
};
describe("my info", ()=>{
test("my name", ()=>{
expect(my.name).toBe("james")
});
test.skip("my name skip", ()=>{
expect(my.name).toBe("james")
});
});
2.2 Jest 匹配器
断言库是Jest自己的,区别于Chai断言库 www.jestjs.cn/docs/expect
注意:
Jest: toBe(value)
Chai: to.be.a(value)
常用的:
expect({a:1}).toBe({a:1}) // 判断两个对象是否相等
expect(1).not.toBe(2) // 判断不等
expect(n).toBeNull(); // 判断是否为null
expect(n).toBeUndefined(); // 判断是否为undefined
expect(n).toBeDefined(); // 判断结果与toBeUndefined相反
expect(n).toBeTruthy(); // 判断结果为true
expect(n).toBeFalsy(); // 判断结果为false
expect(value).toBeGreaterThan(3); // 大于3
expect(value).toBeGreaterThanOrEqual(3.5); // 大于等于3.5
expect(value).toBeLessThan(5); // 小于5
expect(value).toBeLessThanOrEqual(4.5); // 小于等于4.5
expect(value).toBeCloseTo(0.3); // 浮点数判断相等
expect('Christoph').toMatch(/stop/); // 正则表达式判断
expect(['one','two']).toContain('one'); // 含有某个元素
expect(toJson(wrapper)).toMatchSnapshot() // 测试快照
expect(fn).toHaveBeenCalled() // 判断函数是否被调用
expect(fn).toHaveBeenCalledTimes(number) // 判断函数被调用次数
注意:
`toBeNull` 只匹配 `null`
`toBeUndefined` 只匹配 `undefined`
`toBeDefined` 与 `toBeUndefined` 相反
`toBe` 对比的原理是Object.is(value1, value2), 遵循 sameValue算法
2.3 Jest中的[钩子函数]
beforeAll(fn) 在测试用例执行之前运行 多用于做初始化工作
afterAll(fn) 在所有测试用例执行结束之后运行 多用于做测试完毕后的清理工作
beforeEach(fn) 在每一个测试用例执行之前运行
afterEach(fn) 在每一个测试用例执行之后运行
2.4 工具库测试
// common.js
export const add = (a, b) => {
return a + b
}
export const sum = (list) => {
return list.reduce((acc, cur) => acc + cur, 0)
}
export const addItem = (list, item) => {
let ret = [...list]
ret.push(item)
return ret
}
import { add, sum, addItem } from '@/src/utils/common'
describe("test utils", () => {
it("add", () => {
expect(add(1, 2)).toBe(3)
})
it("sum", () => {
expect(sum([1, 2, 3, 4])).toBe(10)
})
it("addItem", () => {
expect(addItem([1, 2, 3], 4)).toHaveLength(4)
})
})
2.5 testEnvironment之jsdom
jsdom 是一个基于 JavaScript 的无头浏览器,可用于创建真实的测试环境。 有了它才可以在node环境下操作DOM
jsdom的简单使用:
const jsdom = require("jsdom");
const { JSDOM } = jsdom;
const dom = new JSDOM(`<!DOCTYPE html><p>Hello world</p>`);
// console.log(dom.window)
// node环境下不能直接通过document来获取DOM节点
console.log(dom.window.document.querySelector("p").textContent); // "Hello world"
const $ = require('jquery')(dom.window);
console.log($('p').text());
有了jsdom测试环境,才使得在jest中可以通过jQuery来获取dom
// 可以通过jquery来获取DOM元素
const $ = require('jquery');
let container;
// 初始化工作
beforeEach(() => {
container = document.createElement('div');
container.className = 'con'
container.innerHTML = 'value'
document.body.appendChild(container);
});
// 测试完成之后的清理工作
afterEach(() => {
document.body.removeChild(container);
container = null;
});
it('container content', () => {
expect($('.con').html()).toBe('value');
});
2.6 支持Promise和async/await
// mock.js
const users = {
4: {
name: 'hehe',
},
5: {
name: 'haha',
},
};
export default function getUserName(userID) {
return new Promise((resolve, reject) => {
process.nextTick(() => {
users[userID] ?
resolve(users[userID].name) :
reject({
error: `User with ${userID} not found.`,
});
});
});
}
import getUserName from './mock'
describe('test Promise', () => {
// 使用'.resolves'来测试promise成功时返回的值
it('works with resolves', () => {
expect(getUserName(5)).resolves.toEqual('haha')
});
// 使用'.rejects'来测试promise失败时返回的值
it('works with rejects', () => {
expect(getUserName(3)).rejects.toEqual({
error: 'User with 3 not found.',
});
});
// 使用promise的返回值来进行测试
it('test resolve with promise', async (done) => {
let data = await getUserName(4);
expect(data).toEqual('hehe');
// 我们必须加入一个done方法,保证我们的回调已经完成了,这时候我们表示测试完成
// 否则jest不知道什么时候停止
done();
});
it('test error with promise', async (done) => {
try {
await getUserName(2);
} catch(e){
expect(e).toEqual({
error: 'User with 2 not found.',
});
}
done();
});
});
三. Mock Funtions
3.1 Jest Mock 的匹配器
Jest 匹配器中还有一类匹配器专门用来检查 jest mock() 的,比如:
名字
mockFn.mockName(value)
mockFn.getMockName()
运行情况
mockFn.mock.calls:传的参数
mockFn.mock.results:得到的返回值
mockFn.mock.instances:mock 包装器实例
模拟函数
mockFn.mockImplementation(fn):重新声明被 mock 的函数
mockFn.mockImplementationOnce(fn)
模拟结果
mockFn.mockReturnThis()
mockFn.mockReturnValue(value)
mockFn.mockReturnValueOnce(value)
mockFn.mockResolvedValue(value)
mockFn.mockResolvedValueOnce(value)
mockFn.mockRejectedValue(value)
mockFn.mockRejectedValueOnce(value)
3.2 jest.fn()
返回一个全新没有使用过的mock function,这个function在被调用的时候会记录很多和函数调用有关的信息
在实际项目的单元测试中,jest.fn()常被用来进行某些有回调函数的测试;
如果没有定义函数内部的实现,jest.fn()会返回undefined作为返回值。
const runCallback = (callback) => {
return callback();
};
test('测试 runCallback', () => {
const mockFn = jest.fn()
runCallback(mockFn)
expect(mockFn).toBeCalled()
expect(mockFn()).toBeUndefined()
// console.log(func.mock)
})
test('测试jest.fn()返回固定值', () => {
let mockFn = jest.fn().mockReturnValue('default');
// 断言mockFn执行后返回值为default
expect(mockFn()).toBe('default');
})
test('测试jest.fn()内部实现', () => {
let mockFn = jest.fn((num1, num2) => {
return num1 * num2;
})
// 断言mockFn执行后返回100
expect(mockFn(10, 10)).toBe(100);
})
test('测试 mock', () => {
const func = jest.fn((x) => {
return x;
});
func('a');
func('b');
expect(func.mock.calls.length).toBe(2);
expect(func.mock.calls[0][0]).toEqual('a');
expect(func.mock.calls[0][1]).toEqual('b');
// console.log(func.mock);
})
test('测试jest.fn()返回Promise', async () => {
let mockFn = jest.fn().mockResolvedValue('hello promise');
let result = await mockFn();
// 断言mockFn通过await关键字执行后返回值为 hello promise
expect(result).toBe('hello promise');
// 断言mockFn调用后返回的是Promise对象
expect(Object.prototype.toString.call(mockFn())).toBe("[object Promise]");
})
测试axios请求
// fetch
import axios from 'axios';
export default {
async fetchPostsList(callback){
return axios.get('https://sijwywb.ecej.com/jwywb/city/getOpenCityList').then(res => {
return callback(res.data);
})
}
}
import fetch from './src/fetch'
test('测试axios请求', async (done) => {
let mockFn = jest.fn();
await fetch.fetchPostsList(mockFn);
// 断言mockFn被调用
expect(mockFn).toBeCalled();
expect(mockFn.mock.calls[0][0]['resultCode']).toBe(200);
// console.log('22', mockFn.mock.calls[0][0]);
// done的目的是告诉Jest异步请求完成,否则会一直等待
done();
})
3.3 jest.mock(moduleName)
使用jest.mock()去mock整个模块 在jest中如果想捕获函数的调用情况,则该函数必须被mock或者spy
// events.js
import fetch from './fetch';
export default {
async getPostList() {
return fetch.fetchPostsList(data => {
console.log('fetchPostsList be called!');
});
}
}
import events from './src/events';
import fetch from './src/fetch';
jest.mock('./src/fetch.js');
test('mock 整个 fetch.js模块', async (done) => {
await events.getPostList();
expect(fetch.fetchPostsList).toHaveBeenCalled();
expect(fetch.fetchPostsList).toHaveBeenCalledTimes(1);
await events.getPostList();
expect(fetch.fetchPostsList).toHaveBeenCalledTimes(2);
done();
});
3.4 jest.spyOn(object, methodName)
jest.spyOn()方法同样创建一个mock函数,但是该mock函数不仅能够捕获函数的调用情况,还可以正常的执行被spy的函数。
返回一个mock function,和jest.fn相似,但是能够追踪object[methodName]的调用信息,类似Sinon
当需要测试某些必须被完整执行的方法时,常常需要使用jest.spyOn()。
import events from './src/events';
import fetch from './src/fetch';
test('使用jest.spyOn()监控fetch.fetchPostsList被正常调用', async(done) => {
const spyFn = jest.spyOn(fetch, 'fetchPostsList');
await events.getPostList();
expect(spyFn).toHaveBeenCalled();
expect(spyFn).toHaveBeenCalledTimes(1);
done();
});