Jest 介绍
Jest是Facebook出品的一个JavaScript测试框架,相对其他测试框架其最大的特点是内置了常用的测试工具开箱即用,比如零配置、自带断言、测试覆盖率工具等功能。
它适用于使用以下项目:Babel、TypeScript、Node、React、Angular、Vue等等!
Jest 主要特点:
- 零配置
- 自带断言
- 快照测试功能,通过对比UI代码生成的快照文件,实现对React等常见框架的自动车测试
- Jest 测试用例是并行执行的,而且只执行发生改变的文件所对应的测试,提升测试速度。
- 测试覆盖率工具
- Mock 模拟
安装
npm i jest @types/jest -D
生成配置文件:
npx jest --init
默认运行所有测试
jest
仅运行指定文件名称或文件路径的测试
# 指定测试文件的名称
jest my-test
# 指定测试文件的路径
jest path/to/my-test.js
仅运行在 hg/git 上有改动但尚未提交的文件
jest -o
仅运行与 path/to/fileA.js
和 path/to/fileB.js
相关的测试
jest --findRelatedTests path/to/fileA.js path/to/fileB.js
仅运行匹配特定名称的测试用例(主要是匹配 describe 或 test 的名称)
jest -t name-of-spec
监视模式
运行监视模式
jest --watch # 默认执行 jest -o 监视有改动的测试
jest --watchAll # 监视所有测试
watchAll 模式
监视文件的更改并在任何更改时重新运行所有测试。 若果你是想要重新运行仅基于已更改文件的测试,那么请使用 --watch
选项。
watch 模式
该模式下可以指定名称或路径来监视特定的测试文件
监视文件更改,并重新运行与已更改的文件相关的测试。 当文件发生更改时,如果你想要重新运行所有测试,可以使用 --watchAll
选项。
注意 watchAll 模式是需要Git支持的,它监视Git仓库中的文件更改,并重新运行已更改的文件相关测试。
监听模式中的辅助命令
在监听模式中,我看可以看到一下指令提示:
- a 运行所有测试用例
- f 检测运行错误的测试用例
- o 仅运行测试修改过的文件
- p 以文件名正则表达式模式进行测试文件的过滤。(会根据文件名与绝对路径来进行匹配)
- t 根据测试的名称(即test函数的第一个参数)来过滤
- q 退出监听模式
- Enter 重新运行测试
要退出以上命令进入的过滤模式,只需要按提示按 w 再按 c 清除即可。
使用ES6模块
npm i babel-jest @babel/core @babel/preset-env -D
// babel.config.js
module.exports = {
presets:[
['@babel/preset-ent',{
targets:{
node:'current'
}
}]
]
}
Jest 使用
常用匹配器
toBe
匹配值(string、number、boolean)
最简单测试一个值的方法是使用精确匹配的方法。
test('two plus two is four', () => {
expect(2 + 2).toBe(4);
});
在上面的代码中,expect (2 + 2)
返回了一个"预期"的对象。 预期的值为4(toBe(4))
运行Jest时,它会跟踪所有失败的用例,打印出错误日志。
如果要检测对象的值,则使用 toEqual
test('对象赋值', () => {
const data = {one: 1};
data['two'] = 2;
expect(data).toEqual({one: 1, two: 2});
});
还可以使用 not 取反
test('不等于0', () => {
expect(2 + 2).not.toBe(0);
});
真值
代码中的undefined
、null
、false
有不同含义,若你在测试时不想区分他们,可以用真值判断。
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(n).toBeFalsy();
expect(z).toBeFalsy();
// 以上匹配结果均通过
});
数值
大多数的比较数字有等价的匹配器。
toBeGreaterThan
大于
toBeGreaterThanOrEqual
大于等于
toBeLessThan
小于
toBeLessThanOrEqual
小于等于
toBeCloseTo
test('two plus two', () => {
const value = 2 + 2;
expect(value).toBeGreaterThan(3);
expect(value).toBeGreaterThanOrEqual(3.5);
expect(value).toBeLessThan(5);
expect(value).toBeLessThanOrEqual(4.5);
// toBe 和 toEqual 一样可以进行数值比较
expect(value).toBe(4);
expect(value).toEqual(4);
});
对于比较浮点数相等,使用 toBeCloseTo
而不是 toEqual
,因为你不希望测试取决于一个小小的舍入误差。
test('两个浮点数字相加', () => {
const value = 0.1 + 0.2;
// 这句会报错,因为浮点数有舍入误差
//expect(value).toBe(0.3);
expect(value).toBeCloseTo(0.3); // 这句可以运行
});
});
字符串
您可以检查对具有 toMatch
正则表达式的字符串︰
test('there is no I in team', () => {
expect('team').not.toMatch(/I/);
});
test('but there is a "stop" in Christoph', () => {
expect('Christoph').toMatch(/stop/);
});
数组和可迭代对象
你可以通过 toContain
来检查一个数组或可迭代对象是否包含某个特定项:
const shoppingList = [ 'diapers', 'kleenex', 'trash bags', 'paper towels', 'milk',];
test('shoppingList数组中包含milk', () => {
expect(shoppingList).toContain('milk');
expect(new Set(shoppingList)).toContain('milk');
});
异常
若你想测试某函数在调用时是否抛出了错误,你需要使用 toThrow
。
function compileAndroidCode() {
throw new Error('you are using the wrong JDK!');
}
test('compiling android goes as expected', () => {
expect(() => compileAndroidCode()).toThrow();
expect(() => compileAndroidCode()).toThrow(Error);
// 您还可以使用必须包含在错误消息中的字符串或regexp
expect(() => compileAndroidCode()).toThrow('you are using the wrong JDK');
expect(() => compileAndroidCode()).toThrow(/JDK/);
// 或者,您可以使用下面这样的regexp来匹配一个确切的错误消息
expect(() => compileAndroidCode()).toThrow(/^you are using the wrong JDK$/); // Test fails
expect(() => compileAndroidCode()).toThrow(/^you are using the wrong JDK!$/); // Test pass
});
匹配器的完整列表,请查阅 参考文档。
测试异步代码
Promise
测试返回一个Promise,则Jest会等待Promise的resove状态 如果 Promise 的状态变为 rejected, 测试将会失败。
例如,有一个名为fetchData
的Promise, 假设它会返回内容为'peanut butter'
的字符串 我们可以使用下面的测试代码︰
test('the data is peanut butter', () => {
return fetchData().then(data => {
expect(data).toBe('peanut butter');
});
});
Async/Await
或者,您可以在测试中使用 async
和 await
。 写异步测试用例时,可以在传递给test
的函数前面加上async
。 例如,可以用来测试相同的 fetchData
方案︰
test('the data is peanut butter', async () => {
const data = await fetchData();
expect(data).toBe('peanut butter');
});
test('the fetch fails with an error', async () => {
expect.assertions(1);
try {
await fetchData();
} catch (e) {
expect(e).toMatch('error');
}
});
你也可以将 async
and await
和 .resolves
or .rejects
一起使用。
test('the data is peanut butter', async () => {
await expect(fetchData()).resolves.toBe('peanut butter');
});
test('the fetch fails with an error', async () => {
await expect(fetchData()).rejects.toMatch('error');
});
上述示例中,async
and await
实际上是一种基于Promise的异步语法糖。
如果是使用类似 settimeout 的方式:
const getdata = (call)=>{
setTimeout(()=>{
call({ val: 111 })
},3000)
}
// 错误示例,Jest并不会等待setTimeout 3s的时间
// test('异步数据', ()=>{
// getdata((data)=>{
// expect(data).toEqual({ val: 111 })
// })
// })
// 正确做法是传入done调用
test('异步数据', (done)=>{
getdata((data)=>{
done()
expect(data).toEqual({ val: 111 })
})
})
.resolves
/ .rejects
您还可以使用 .resolves
匹配器在您期望的声明,Jest 会等待这一 Promise 来解决。 如果 Promise 被拒绝,则测试将自动失败。
test('the data is peanut butter', () => {
return expect(fetchData()).resolves.toBe('peanut butter');
});
一定不要忘记把整个断言作为返回值返回⸺ return
语句的话,在 fetchData
返回的这个 promise 变更为 resolved 状态、then() 有机会执行之前,测试就已经被视为已经完成了。
如果你希望Promise返回rejected,你需要使用 .rejects
匹配器。 它和 .resolves
匹配器是一样的使用方式。 如果 Promise 被拒绝,则测试将自动失败。
test('the fetch fails with an error', () => {
return expect(fetchData()).rejects.toMatch('error');
});
上述几种异步形式没有优劣之分,你可以在你的项目或者文件中混合或单独使用他们。 这只取决于哪种形式更能使您的测试变得简单。
定时器模拟 - Mock
- useFakeTimers - 启用假定时器代替setTimeout
- useRealTimers - 恢复使用原始定时器
- runAllTimers - 快进使得所有定时器执行结束
- runOnlyPendingTimers - 快进使得当前进行的定时器执行结束(不管其他的定时器),常用于解决递归造成的无限定时器循环
- advancertimersbytime - 指定快进的时间
使用以上定时器模拟,可以避免测试settimeout是的等待时间,可以控制提前结束定时器获得结算结果。
以下使用实例:
const getdata = (call)=>{
setTimeout(()=>{
call({ val: 111 })
setTimeout(()=>{
console.log(1234)
},1000)
},3000)
}
// 启用假定时器
jest.useFakeTimers()
// 示例1 ----- useRealTimers
test('异步数据', ()=>{
getdata((data)=>{
expect(data).toEqual({ val: 111 })
})
// 快进所有定时器结束
jest.runAllTimers();
})
// 示例2 ----- runOnlyPendingTimers
jest.useFakeTimers()
test('异步数据', ()=>{
getdata((data)=>{
expect(data).toEqual({ val: 111 })
})
// 结束当前进行的定时器,因此getdata中的内层(第二个定时器不会再执行)
jest.runOnlyPendingTimers();
})
// 示例3 ----- advancertimersbytime
test('异步数据', ()=>{
getdata((data)=>{
expect(data).toEqual({ val: 111 })
})
// 指定快进2s
jest.advancertimersbytime(2000);
// 再次指定快进1s,此时总共快进了3s,所以getdata中的第一个定时器会完成执行。如果再次快进1s,则内部第二个定时器也会完成执行
jest.advancertimersbytime(1000);
})
模拟函数 - Mock
假设我们要测试函数 forEach
的内部实现,这个函数为传入的数组中的每个元素调用一次回调函数。
export function forEach(items, callback) {
for (let index = 0; index < items.length; index++) {
callback(items[index]);
}
}
当我们想校验函数执行的次数是否正确时,就可以使用 mock函数 jest.fn
,mock函数调用执行后会存在以下运行结果,其中calls
中储存了函数回调所接受的参数并以数组的形式储存。results
是mockFn函数执行的返回结果。
test('forEach 测试校验',()=>{
const arr = [1,2,3]
const mockFn = jest.fn((val ,ii)=>{
return val * 10
})
.mockName('forEach') // 指定mockFn的名称,当mockFn内部报错时,提示错误的名称
forEach(arr, mockFn)
// 校验执行次数是否正确
expect(mockFn.mock.calls.length).toBe(arr.length)
console.log(mockFn.mock)
})
模拟模块 - Axios请求
假定有个从 API 获取用户的类。 该类用 axios 调用 API 然后返回 data
,其中包含所有用户的属性:
import axios from 'axios'
import { getUserInfo } from './user'
// 模拟模块
jest.mock('axios')
test('获取用户信息接口测试', async () => {
const userInfo = { name: '张三' }
const resData = { data:userInfo }
// mock 响应结果
axios.get.mockResolvedValue(resData)
const resultData = await getUserInfo()
expect(resultData.data).toEqual(userInfo)
})
模拟实现 - mock
当你需要根据别的模块定义默认的Mock函数实现时,mockImplementation
方法是非常有用的。
// foo.js
module.exports = function () {
// some implementation;
};
// test.js
jest.mock('../foo'); // this happens automatically with automocking
const foo = require('../foo');
// foo is a mock function
foo.mockImplementation(() => 42);
foo();
// > 42
当你需要模拟某个函数调用返回不同结果时,请使用 mockImplementationOnce
方法︰
const myMockFn = jest
.fn()
.mockImplementationOnce(cb => cb(null, true))
.mockImplementationOnce(cb => cb(null, false));
myMockFn((err, val) => console.log(val));
// > true
myMockFn((err, val) => console.log(val));
// > false
当 mockImplementationOne
定义的实现逐个调用完毕时, 如果定义了jest.fn
,它将使用 jest.fn
。
const myMockFn = jest
.fn(() => 'default')
.mockImplementationOnce(() => 'first call')
.mockImplementationOnce(() => 'second call');
console.log(myMockFn(), myMockFn(), myMockFn(), myMockFn());
// > 'first call', 'second call', 'default', 'default'