1.1 前端自动化测试背景
开发复杂的业务场景时或者对老代码进行修改时,容易产生Bug,为了在上线之前发现Bug,可以采取的措施:
- 通过测试同学的验证发现
- 通过多人code view发现
- 灰度发布
- 借助工具TypeScript、Flow、ESlint、StyleLint避免
- 使用前端自动化测试工具,进一步发现Bug,单元测试、集成测试、端到端的测试
没有前端自动化测试时,只有验证功能出现问题或者线上产生了Bug才能发现代码中的问题。
使用前端自动化测试,可以写测试代码,通过测试代码运行业务功能中编写的代码,检查代码输出的结果是否和预期的是一致的
1.2 前端自动化测试框架Jest
一款优雅、简洁的JavaScript的测试框架,支持Babel、TybeScript、Node、React、Angular、Vue等诸多框架。
优点:
- 速度快、API简单、易配置、隔离性好 (windows上运行感觉不快啊?)
- 监控模式、IDEA整合、Snapshot、多项目并行、覆盖率、Mock丰富
安装:
npm init
npm install jest@24.8.0 -D # -D 只在开发环境下才会运行单元测试
npm install jest@24.8.0 --save-dev # 同样只在开发环境运行
"test": "jest" # package.json中scripts中增加命令
运行一个案例:
待测试逻辑代码:
function add(a, b) {
return a + b
}
function sub(a, b) {
return a - b
}
function multi(a, b) {
return a * b
}
module.exports = {
add,
sub,
multi
}
测试代码:
const { add, sub, multi } = require('./math')
test('测试加法1 + 2', () => {
expect(add(1, 2)).toBe(3)
})
test('测试减法7 - 3', () => {
expect(sub(7, 3)).toBe(4)
})
test('测试乘法2 * 5', () => {
expect(multi(2, 5)).toBe(10)
})
运行测试代码:
> lesson2@1.0.0 test E:\project\frontendautomatedtest\lesson2
> jest
PASS ./math.test.js
√ 测试加法1 + 2 (4ms)
√ 测试减法7 - 3
√ 测试乘法2 * 5 (1ms)
Test Suites: 1 passed, 1 total
Tests: 3 passed, 3 total
Snapshots: 0 total
Time: 7.226s
Ran all test suites.
1.3 Jest的简单配置
生成基础配置文件
npx jest --init # 暴露jest配置,jest.config.js
在jest.config.js中可以指定生成测试覆盖率报告的保存路径
coverageDirectory: "coverage" # 测试覆盖率报告路径在当前文件夹下的coverage下
npx jest --coverage # 运行命令,执行测试,并生成测试覆盖率报告
使用Babel
# 1. 安装所需要的依赖
npm install @babel/core@7.4.5 @babel/preset-env@7.4.5 -D # 引入babel相关的库,将math.js和math.test.js 修改为ESModule导出和导入的形式,因为node环境下仅支持commonjs的语法,参考代码lesson3
# 2. 新建.babelrc babel配置文件
{
"presets": [ # 插件预设 预设 一组插件集合
["@babel/preset-env", { # 内部数组的第二项是当前预设的配置
"targets": { # 根据当前node环境使用@babel/preset-env转换当前的代码,例如将import语法转成
"node": "current" # commonsjs的语法
}
}]
]
}
# 3. 运行测试,看ESModule导出和导入的语法是否会报错
npm run test
讲师分析原理,当运行npm run jest时,jest中的babel-jest模块,会检测当前目录下是否包含bable和bable-core依赖,有的话,就取.babelrc中的配置,在运行测试之前,使用babel 先把待测试代码做一次转化,然后再运行转换后的测试用例代码。
1.4 Jest中常用的匹配器
Jest中提供了大量的匹配器,用于判断测试案例运行的实际结果和预期结果是否匹配,Jest会跟踪所有失败的匹配器,然后打印出明确的错误消息。
常用的匹配器如下:
toBe 精确匹配,类似于Object.is,如果是对象或者数组的话,需要都是同一个对象或者数组的引用
toEqual 可用于检查对象或者数组的值(内容)是否相同,会递归的检查每个字段
toBeNull 只匹配null
toBeUndefined 只匹配undefined
toBeDefined 与toBeUndefined相反
toBeTruthy 真,匹配任何if语句为真
toBeFalsy 假,与toBeTruthy相反
not 非,反向匹配器
toBeGreaterThan 大于
toBeLessThan 小于
toBeGreaterThanOrEqual 大于等于
toBeLessThanOrEqual 小于等于
toBeCloseTo 比较浮点数相等,避免浮点数运算的舍入误差,例如0.1 + 0.2 === 0.3
toMatch 检查字符串匹配 支持正则
toContain 检查数组或集合是否包含某一项 ,支持正则
toThrow 检查函数在调用时,是否抛出了某个个异常,支持正则
所有匹配器,参考官方文档Expect断言
1.5 Jest命令行工具使用
Jest命令行工具可以指定选项,从而按照指定的方式运行测试。
常用命令说明:
jest # 运行所有测试文件
jest matcher.test.js # 只运行matcher.test.js这个测试文件
jest -o # 只运行git追踪到改到的文件,项目需要已经创建了git仓库
jest -t toMatch # 运行匹配test或describe名称中包含toMatch字符串的测试案例
jest --watch # 监视当前文件加下有改动的测试文件 类似jest -o
jest --watchAll # 监视所有测试
选项比较多,可以使用jest --help查看所有命令选项,也可以参考官方文档Jest CLI选项
使用jest --watch启动监控模式后,会显示如下选项,可以选择不同的模式运行测试文件
Watch Usage
› Press a to run all tests.
› Press f to run only failed tests.
› Press p to filter by a filename regex pattern.
› Press t to filter by a test name regex pattern.
› Press q to quit watch mode.
› Press Enter to trigger a test run.
模式说明如下:
a模式:每次运行所有的测试文件
f模式:进运行失败的测试文件
p模式 :配合测试文件的文件名,输入正则表达式,过滤文件名
t模式 :匹配test的名称,输入正则表达式,过滤test名称
q:退出watch模式
1.6 测试异步代码
默认情况下,当测试代码运行到上下文的底部时,Jest认为测试代码执行完成了,但是此时异步代码还未执行完成,Jest官网建议使用以下三种方法解决这个问题。
方法1 使用done回调
Jest会等done回调函数执行结束后,再结束测试。
test('fetchData返回结果为 { success:true }', done => {
// 添加一个回调参数done,只有执行了done才认为这个测试用例结束了
fetchData(data => {
expect(data).toEqual({
success: true
})
done()
})
})
方法2 返回Promise
Jest会等Promised的resolve状态,如果Promise的状态变成了rejected,测试将会失败。
test('fetchDataPromise { success: true }', () => {
// return 一个promise
return fetchDataPromise().then(res => {
expect(res.data).toEqual({ success: true })
})
})
方法3 使用Async/Await
test('fetchDataPromise { success: true }', () => {
// return 一个promise
return fetchDataPromise().then(res => {
expect(res.data).toEqual({ success: true })
})
})
如果期望Promise被reject,则需要使用catch方法,请确保添加了expect.assertions检查断言被调用的次数,否则变成fulfilled状态的Promise不会让测试用例失败。
test('fetchDataPromise404 返回404', async () => {
expect.assertions(1)
try {
await fetchDataPromise404()
} catch (e) {
expect(e.toString()).toEqual('Error: Request failed with status code 404')
}
})
也可以将 async
and await
和 .resolves
or .rejects
一起使用,来处理异步期望的结果
test('fetchDataPromise404 返回404', async () => {
return expect(fetchDataPromise404()).rejects.toThrow()
})
1.7 Jest中的钩子函数
如果需要在运行测试案例之前,准备一些测试数据或做一些准备工作,Jest提供了一些辅助函数,如beforeAll、beforeEach、afterEach、afterAll,类似vue-router中的路由钩子函数。
beforeAll 在运行所有test案例之前运行一次,可以做所有测试开始前的准备工作
afterAll 在运行完成所有test案例之前运行一次,可以做所有测试完成后的清理工作
beforeEach 在运行每一个test案例之前都会被调用
afterEach 运行完每一个test案例之后都会被调用
作用域说明
在文件顶层定义的before和after的hook函数会作用在每个test案例上,在describe块中声明的hook函数,只会作用于describe中的test案例上。
并且,顶级的beforeEach比describe中的beforeEach执行的早
执行顺序
在运行test测试案例之前,会先执行describe中的代码,describe中的代码执行完成后,默认情况下Jest会按照顺序依次运行test案例。
如下案例:
describe('describe outer', () => {
console.log('describe outer-a');
describe('describe inner 1', () => {
console.log('describe inner 1');
test('test 1', () => console.log('test 1'));
});
console.log('describe outer-b');
test('test 2', () => console.log('test 2'));
describe('describe inner 2', () => {
console.log('describe inner 2');
test('test 3', () => console.log('test 3'));
});
console.log('describe outer-c');
});
// describe outer-a
// describe inner 1
// describe outer-b
// describe inner 2
// describe outer-c
// test 1
// test 2
// test 3
如果运行了较为复杂的test,出现失败的情况后,官网建议使用test.only仅运行此条测试观察是否会报错,如果没有报错,然后运行整个测试文件会报错,可以考虑是否是外部条件的设置导致的。
1.8 Jest中的模拟函数Mock
模拟函数调用
有时我们需要模拟函数的实现,比如函数的返回值,函数被调用了多少次,函数调用时传入的参数等情况,Jest提供了Mock模拟函数jest.fn()来帮助我们测试函数的调用情况。
例如我们模拟一个返回2次方的函数,但是要求,入参是2返回1,入参是3返回2,入参是4返回函数本身执行的结果,代码如下:
test('模拟2次方', () => {
const fn = jest.fn(num => {
return Math.pow(num, 2)
})
fn.mockReturnValueOnce(1) // 模拟一次函数返回结果是1
expect(fn(2)).toBe(1)
fn.mockReturnValueOnce(2) // 模拟一次函数返回结果是2
expect(fn(3)).toBe(2)
expect(fn(4)).toBe(16)
console.log(fn.mock)
})
在上面的例子中,我们打印了模拟函数fn的mock属性,mock属性是所有模拟函数都有的一个属性,它保存了模拟函数每次被调用时的入参、调用方、调用顺序和返回结果。上面的打印信息如下:
{
calls: [ [ 2 ], [ 3 ], [ 4 ] ], # 每次调用时的入参
instances: [ undefined, undefined, undefined ], # 调用方法,this,此处是node环境
invocationCallOrder: [ 1, 2, 3 ], # 调用顺序
results: [ # 每次调用时的返回结果
{ type: 'return', value: 1 },
{ type: 'return', value: 2 },
{ type: 'return', value: 16 }
]
}
因此,根据这些信息就可以测试函数的调用过程,例如
...
expect(fn.mock.calls).toHaveLength(3) // 判定函数被调用了3次
expect(fn.mock.calls[2]).toEqual([4]) // 判定第3次调用时,入参为4
expect(fn.mock.results[2].value).toBe(16) // 判定第3次调用,返回值为16
// 检查this指向
test("检查this", () => {
const Dog = jest.fn();
const a = new Dog();
const b = new Dog();
expect(Dog.mock.instances[0]).toBe(a);
expect(Dog.mock.instances[1]).toBe(b);
const Cat = jest.fn();
const c = {};
const bound = Cat.bind(c);
bound();
});
模拟模块
几乎所有的前端项目都会有一个请求后端数据的模块requestUtil,假设该模块有个获取用户信息的getUserInfo方法,该方法调用了axios的get方法请求后端接口,返回用户的信息数据data。为测试该方法而不实际调用 API (使测试缓慢与脆弱),可以用 jest.mock(...)
函数自动模拟 axios 模块。
import { getUserInfo } from './requestUtil'
import axios from "axios"
jest.mock('axios')
test('测试getUserInfo方法', async () => {
axios.get.mockResolvedValueOnce({data: { nickName: 'hewang', id: 1}})
axios.get.mockResolvedValueOnce({data: { nickName: '曹操', id: 2}})
await getUserInfo().then(data => {
expect(data).toEqual({ nickName: 'hewang', id: 1})
})
await getUserInfo().then(data => {
expect(data).toEqual({ nickName: '曹操', id: 2})
})
})
Jest还提供了很多匹配器,方便判定模拟函数的调用情况
// mock方法至少被调用一次
expect(mockFunc).toHaveBeenCalled();
// mock方法至少被调用一次并且入参为arg1和arg2
expect(mockFunc).toHaveBeenCalledWith(arg1, arg2);
// 最后一次调用入参是arg1和arg2
expect(mockFunc).toHaveBeenLastCalledWith(arg1, arg2);
代码参考gitee仓库
参考文档Jest官方文档