前言: 问了好些前端大神,回复都说,做好前端测试是个不简单的事情,特别是对于前端UI部分的测试。antd在UI上,可以说做得很好,但是看了他们的相关测试,也没有很完善。.
目前来说,前端测试在service和model层的应用,结合相关资料和自己遇到的坑,给大家分享一下。
一、配置
(1) jest的版本选择("jest": "^20.0.0"):
项目中,用到的 babel等编译工具不是最新版本(6.0.0+),在当前最新版本24.0.0下,会报编译工具不合适的错误,把版本降至20.0.0,可以使用。具体版本,可以根据项目中版本进行选择。
(2) .babelrc 的配置
项目中的预编译,主要是由babelrc 文件中配置,presets和plugins两项,这两项跟项目中,webpack中的配置保持一致
presets: ['es2015', 'stage-3'],
plugins: ['transform-class-properties',
'syntax-dynamic-import',
'transform-runtime'
],
复制代码
(3) jest.config 的配置
如果项目中在node_modules中依赖了未编译的插件,需要进行下面的设置。否则,在.test用例中,无法引入依赖,从而会报错。
transform: { // 将.js后缀的文件使用babel-jest处理
"^.+\\.js$": "babel-jest",
},
transformIgnorePatterns: // 未编译的依赖,需要在这边配置 ["<rootDir>/node_modules/(?!(snk-sse|javascript-state-machine))"],
复制代码
(4)package 的配置
"jest": "jest --coverage"
复制代码
运行npm run jest,就会运行带有.test.js的所有文件
二、使用
(1)调用模拟接口
在node里面,没有实际接口调用,如果测试涉及到接口调用的返回数据,就需要进行模拟数据。下面,介绍其中一种比较简单的方法:
第一步,在 VideoService.js的同级目录,新建 mocks 文件夹,然后在文件夹里面,新建 VideoService.js(mock文件夹的文件名必须与的实际文件名一致)
// __mocks__/VideoService.js文件,
export default {
httpQuery() {
return new Promise((resolve)=>{
resolve(123123);
})
}
}
复制代码
第二步,在测试test.js文件中,需要加上jest.mock(),RtVideoService.httpChange调用了VideoService.js的接口,这里会查找__mocks__下面的文件,并使用VideoService模拟的数据
jest.mock('../services/VideoService.js'); // 必须存在,要不然会报错
test('RtVideoService', () => {
const obj = {
newer: 'linwenhuan@sinosafe.com.cn',
callback: { error: jest.fn() }
};
await RtVideoService.httpChange(null, obj); // 使用了__mocks__下的文件
expect(obj.callback.next.mock.calls.length).toBe(1);
})
复制代码
(2)运行单条测试用例
如果测试失败,第一件要检查的事就是,当仅运行这条测试时,它是否仍然失败。 在 Jest 中很容易地只运行一个测试 — — 只需暂时将 test 命令更改为 test.only:
test.only('this will be the only test that runs', () => {
expect(true).toBe(false);
});
test('this test will not run', () => { // 此条不会运行
expect('A').toBe('A');
});
复制代码
(3)常用的断言
普通匹配器
- .toBe - toBe 使用 Object.is 来测试是否完全相等
- .not - 用来测试相反的用例
- .toEqual - 如果你想检查某个对象的值,请改用 toEqual。
test('two plus two is four', () => {
expect(2 + 2).toBe(4);
});
test('object assignment', () => {
const data = {one: 1};
data['two'] = 2;
expect(data).toEqual({one: 1, two: 2});
});
复制代码
布尔值匹配器
- toBeNull 只匹配 null
- toBeUndefined 只匹配 undefined
- toBeDefined 与 toBeUndefined 相反
- toBeTruthy 匹配任何 if 语句为真
- toBeFalsy 匹配任何 if 语句为假
test('null', () => {
const n = null;
expect(n).toBeNull();
expect(n).toBeDefined();
expect(n).not.toBeUndefined();
expect(n).not.toBeTruthy();
expect(n).toBeFalsy();
});
test('zero', () => {
const z = 0;
expect(z).not.toBeNull();
expect(z).toBeDefined();
expect(z).not.toBeUndefined();
expect(z).not.toBeTruthy();
expect(z).toBeFalsy();
});
复制代码
字符串匹配器
- toMatch - 正则表达式的字符
- toHaveLength(number) - 判断一个有长度的对象的长度
数字匹配器
- .toBeGreaterThan() - 大于
- .toBeGreaterThanOrEqual() 大于等于
- .toBeLessThan() - 小于
- .toBeLessThanOrEqual() - 小于等于
- .toBeCloseTo() - 浮点数比较
数组匹配器
- .toContain(item) - 判断数组是否包含特定子项
- .toContainEqual(item) - 判断数组中是否包含一个特定对象
自定义断言
expect.extend({
toBeDivisibleBy(received, argument) {
const pass = received % argument == 0;
if (pass) {
return {
message: () =>
`expected ${received} not to be divisible by ${argument}`,
pass: true,
};
} else {
return {
message: () => `expected ${received} to be divisible by ${argument}`,
pass: false,
};
}
},
});
test('even and odd numbers', () => {
expect(100).toBeDivisibleBy(2);
expect(101).not.toBeDivisibleBy(2);
});
复制代码
(4)上面有说到模拟接口,其他模拟方法
1、jest.fn()
// 举例
test('测试jest.fn()调用', () => {
let mockFn = jest.fn();
let result = mockFn(1, 2, 3);
// 断言mockFn的执行后返回undefined
expect(result).toBeUndefined();
// 断言mockFn被调用
expect(mockFn).toBeCalled();
// 断言mockFn被调用了一次
expect(mockFn).toBeCalledTimes(1);
// 断言mockFn传入的参数为1, 2, 3
expect(mockFn).toHaveBeenCalledWith(1, 2, 3);
})
// 实际运用
test('answer', () => {
const fn = { error: jest.fn() };
ExeService.answer(null, fn);
expect(fn.error.mock.calls.length).toBe(1); // 测试成功
});
复制代码
jest.fn()所创建的Mock函数还可以设置返回值,定义内部实现或返回Promise对象。
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('测试jest.fn()返回Promise', async () => {
let mockFn = jest.fn().mockResolvedValue('default');
let result = await mockFn();
// 断言mockFn通过await关键字执行后返回值为default
expect(result).toBe('default');
// 断言mockFn调用后返回的是Promise对象
expect(Object.prototype.toString.call(mockFn())).toBe("[object Promise]");
})
复制代码
2、jest.mock(),改变函数的内部实现,上面已经有实际案例,不估重复讲述
3、jest.spyOn()
jest.spyOn()方法同样创建一个mock函数,但是该mock函数不仅能够捕获函数的调用情况,还可以正常的执行被spy的函数。实际上,jest.spyOn()是jest.fn()的语法糖,它创建了一个和被spy的函数具有相同内部代码的mock函数
jest.mock('../services/VideoService.js'); // 必须存在,要不然会报错
test('RtVideoService', () => {
const obj = {
newer: 'linwenhuan@sinosafe.com.cn',
callback: { error: jest.fn() }
};
await RtVideoService.httpChange(null, obj); // 使用了__mocks__下的文件
const spyFn = jest.spyOn(VideoService, 'httpChange');
expect(spyFn).toHaveBeenCalled();
expect(obj.callback.next.mock.calls.length).toBe(1);
})
复制代码
三、覆盖率检查
在执行完写的测试用例后,你可能会想,有什么指标来说明我写的测试用例,覆盖项目全不全?
"test": "jest --coverage"
复制代码
1、首先,在package的命令上,带有coverage,这时,执行成功全部用例,会输出以下结果:
%stmts是语句覆盖率(statement coverage):是不是每个语句都执行了?
%Branch分支覆盖率(branch coverage):是不是每个if代码块都执行了?
%Funcs函数覆盖率(function coverage):是不是每个函数都调用了?
%Lines行覆盖率(line coverage):是不是每一行都执行了?
2、coverage 需要忽略的文件或文件夹
coveragePathIgnorePatterns: [
"\\\\node_modules\\\\",
"<rootDir>/src/utils/",
"<rootDir>/src/observers/",
"<rootDir>/lib/",
],
复制代码
3、执行完测试用例后,会在项目要目录,生成coverage文件夹,里面输出覆盖率报告
四、从中得到的体会
- 1、为了进行jest、 Mock,如果代码不适合测试,会对代码进行重构,这在一定程度使自己的代码结构更加趋于合理;
- 2、单元测试可以给出每项测试的响应时间,合理划分的单元测试有助于定位代码的性能问题;
- 3、单元测试还是一份很好的业务文档,每项测试的描述都可以体现业务逻辑