Jest API
官方文档:全局设定 · Jest
在测试文件中,Jest 将所有 API 和对象放入全局环境中。开发者不需要导入任何内容即可使用它们。但是,如果您喜欢显式导入,则可以:
import { describe, expect, test } from '@jest/globals'
Test 函数
test 函数用于创建测试用例。
Jest 要求每一个测试文件至少包含一个测试用例,否则会报错,例如创建一个空的测试文件 global.test.js,运行 jest 结果:

test 函数别名: it(name, fn, timeout),以下两个测试用例相同:
test('global test', () => {
console.log('global')
})
it('global test', () => {
console.log('global')
})
Test 函数包括:
test(name, fn, timeout)test.concurrent(name, fn, timeout)test.concurrent.each(table)(name, fn, timeout)test.concurrent.only.each(table)(name, fn)test.concurrent.skip.each(table)(name, fn)test.each(table)(name, fn, timeout)test.only(name, fn, timeout)test.only.each(table)(name, fn)test.skip(name, fn)test.skip.each(table)(name, fn)test.todo(name)
Expect 匹配器
官方文档:
在编写测试时,通常需要检查值是否满足某些条件,即“断言”。 Expect 使您可以访问许多“匹配器”,以使您可以验证不同的内容。
常用匹配器:
// 常规
test('Common Matchers', () => {
// toBe
// 使用 Object.is 精确比较值(对象只匹配引用)
expect(2 + 2).toBe(4)
expect('hello').toBe('hello')
expect(true).toBe(true)
const foo = { age: 18 }
// 失败:对象只匹配引用
// expect(foo).toBe({ age: 18 })
// toEqual
// 递归(使用 Object.is)比较对象实例的所有属性(深度对比)
expect(foo).toEqual({ age: 18 })
// 注意:toEqual 不会对 Error 进行深度对比,只会对比 message 属性是否相等
expect(new Error('报错了')).toEqual(new Error('报错了'))
})
// 真实性
test('Truthiness', () => {
const n = null
// toBeNull 只匹配 null
expect(n).toBeNull()
// toBeDefined 与 toBeUndefined 相反
expect(n).toBeDefined()
// toBeUndefined 只匹配 undefined
expect(n).not.toBeUndefined()
// toBeTruthy 匹配 if 语句视为 true 的内容
expect(n).not.toBeTruthy()
// toBeFalsy 匹配 if 语句视为 false 的内容
expect(n).toBeFalsy()
const z = 0
expect(z).not.toBeNull()
expect(z).toBeDefined()
expect(z).not.toBeUndefined()
expect(z).not.toBeTruthy()
expect(z).toBeFalsy()
})
// 数字
test('Numbers', () => {
const value = 2 + 2
// 大于
expect(value).toBeGreaterThan(3)
// 大于等于
expect(value).toBeGreaterThanOrEqual(4)
// 小于
expect(value).toBeLessThan(5)
// 小于等于
expect(value).toBeLessThanOrEqual(4.5)
// toBe 和 toEqual 在数字上是等价的
expect(value).toBe(4)
expect(value).toEqual(4)
const float = 0.1 + 0.2
// JS 浮点运算会有 round 错误,存在微小误差,使用 toBe 或 toEqual 对比浮点数可能不会正常工作
// expect(value).toBe(0.3)
console.log(float) // 0.30000000000000004
// 对于浮点对比,可以使用 toBeCloseTo 进行近似相等
// 可以传入第二个参数限制小数点后要检查的位数,默认是 2
expect(float).toBeCloseTo(0.3)
// toBeCloseTo 是为了解决浮点错误问题,所以它不支持 bigInt 类型(通常用于定义超出 JS 最大数字 2^53 - 1 的整数)的值
// expect(1n).toBeCloseTo(1n)
})
// 字符串
test('Strings', () => {
// toMatch 正则表达式匹配
expect('team').not.toMatch(/I/)
expect('Christoph').toMatch(/stop/)
})
// 数组和迭代类型
test('Arrays and iterables', () => {
const shoppingList = ['diapers', 'kleenex', 'trash bags', 'paper towels', 'milk']
// toContain 用于检查数组或迭代类型是否包含指定项
expect(shoppingList).toContain('milk')
expect(new Set(shoppingList)).toContain('milk')
})
// 异常
test('Exceptions', () => {
function compileAndroidCode() {
throw new Error('you are using the wrong JDK')
// throw 'you are using the wrong JDK'
}
// toThrow 可以测试调用某个特定函数时是否抛出异常
// 注意:传递的必须是一个函数,jest 会调用它
expect(compileAndroidCode).toThrow()
// or(建议)
expect(() => compileAndroidCode()).toThrow()
// 可以传递可选参数来测试是否抛出了特定错误:
// 1. 正则表达式:错误消息的正则匹配
expect(() => compileAndroidCode()).toThrow(/JDK/)
// 2. 字符串:错误消息是否包含字符串
expect(() => compileAndroidCode()).toThrow('wrong JDK')
// 3. Error 实例:匹配错误消息是否一致
expect(() => compileAndroidCode()).toThrow(new Error('you are using the wrong JDK'))
// 4. Error 类:匹配是否抛出的 Error 实例
expect(() => compileAndroidCode()).toThrow(Error)
})
describe 函数
describe 创建一个将几个相关测试组合在一起的块。
const myBeverage = {
delicious: true,
sour: false,
};
describe('my beverage', () => {
test('is delicious', () => {
expect(myBeverage.delicious).toBeTruthy();
});
test('is not sour', () => {
expect(myBeverage.sour).toBeFalsy();
});
});
describe 函数可以将相关的测试用例进行分组,在测试失败的时候可以获得更直观的提示,除此之外还可以使用作用域,定制私有的生命周期钩子等好处。
describe 函数包括:
describe(name, fn)describe.each(table)(name, fn, timeout)describe.only(name, fn)describe.only.each(table)(name, fn)describe.skip(name, fn)describe.skip.each(table)(name, fn)
生命周期钩子
生命周期钩子主要用于在测试运行完成前后执行一些自定义内容。
beforeAll(() => {
console.log('在当前测试文件的所有用例运行前执行')
})
beforeEach(() => {
console.log('在当前测试文件的所每个用例运行前执行')
})
生命周期钩子包括:
afterAll(fn, timeout)afterEach(fn, timeout)beforeAll(fn, timeout)beforeEach(fn, timeout)
Jest 对象
官方文档:The Jest Object
jest 对象自动位于每个测试文件中的作用域中。 jest 对象中的方法有助于创建模拟(mocks),并让您控制 Jest 的整体行为。也可以通过从 import {jest} from '@jest/globals' 导入。
测试异步代码
Jest 在测试 JavaScript 中的异步代码时,需要知道正在测试的代码何时完成,才能继续进行另一个测试。
Callbacks
最常见的异步模式是 callback 回调。
常见的错误做法:
// 测试异步代码
function fetchData(callback) {
setTimeout(() => {
callback({ foo: 'bar' })
}, 1000)
}
// 错误用例
test('async test', () => {
fetchData(data => {
expect(data).toEqual({ foo: 'bar' })
expect(data).toEqual(1) // 仍会测试通过
})
})
上面的测试没有按照预期工作,是因为默认情况下,Jest 测试在代码执行结束后立即完成,不会等待异步代码执行结果。
所以该测试用例,在调用回调之前,fetchData 就会调用结束,测试就会 complete(完成)。
解决方法是,测试函数接受一个名为 done 的参数,Jest 将在完成测试之前等待 done() 被调用。
注意:因为 Jest 会等待
done,所以一些错误的异步测试用例也会等待相同的时间,这可能会导致这些用例按照期望工作,但使用方式仍是错误的。
建议:虽然 done 的作用是让测试等待异步代码执行完毕,不过还是建议将
done()写在断言调用之后。
// 测试异步代码
function fetchData(callback) {
setTimeout(() => {
callback({ foo: 'bar' })
}, 1000)
}
// 正确用例
test('async test', done => {
fetchData(data => {
try {
expect(data).toEqual(1)
done()
} catch (error) {
done(error)
}
})
})
如果 done() 一直不被调用,测试将失败(带有超时错误)。
如果 expect 语句失败,它会抛出一个错误,并且不会调用 done(),所以必须将 expect 包裹在一个 try-catch 块中,并将 catch 块中的错误传递给 done,否则我们只会得到一个超时错误,而不会显示 expect(data) 接受到的值。
注意:
done()不应与 Promises 混合使用,因为这会导致测试中的内存泄漏。
Promises
如果你的代码使用 Promise,除了使用 done,还有一种更直接的方法来处理异步测试。
返回一个 Promise
从测试中返回一个 Promise,Jest 将等待该 Promise 直到决议(resolve)。如果 Promise 被拒绝(rejected),测试将自动失败。
注意:两种方式不能混用。
// 测试异步代码
function fetchData(isRejected) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (isRejected) {
reject('This throws an error')
} else {
resolve({ foo: 'bar' })
}
}, 1000)
})
}
// done 方式
test('async test', done => {
fetchData().then(data => {
try {
expect(data).toEqual({ foo: 'bar' })
// expect(data).toEqual(1)
done()
} catch (error) {
done(error)
}
})
})
// promise 方式
test('promise', () => {
return fetchData().then(data => {
expect(data).toEqual({ foo: 'bar' })
// expect(data).toEqual(1)
})
})
期望一个拒绝的 Promise
如果你期望(expect)一个 Promise 被拒绝(rejected),则会使用 .catch() 方法捕获。
如果仅期望拒绝,而不期望成功(不使用 .then() 方法),请确保使用 expect.assertions(number) 方法,验证测试用例是否调用了一定数量的断言。否则 fulfilled(或称 resolved) 的 Promise 不会使测试失败。
使用
expect.assertions(number)另一个好处是可以确保你写的异步代码测试用例是否正确。
test('没有断言被调用', () => {
// fetchData() 返回一个成功的 Promise,不会进入 catch
// 又因为没有定义 then,不需进行任何校验,所以该测试将成功
return fetchData()
// .then(data => {
// expect(data).toEqual({ foo: 'bar' })
// })
.catch(data => {
expect(data).toMatch('error')
})
})
test('指定断言调用数量', () => {
// 期望会有且只有一个断言被调用
expect.assertions(1)
// 因为没有进入 catch,所以没有调用任何断言,该测试结果将失败
return fetchData()
.catch(data => {
expect(data).toMatch('error')
})
})
.resolves / .rejects
还有一个方式是使用 expect 语句中的 .resolves/.rejects 匹配器。
.resolves:Jest 会等待 Promise 被 resolve,如果被拒绝,则测试自动失败.rejects:与.resolves相反,如果 Promise 被兑现,则测试自动失败
test('.resolves', () => {
return expect(fetchData()).resolves.toEqual({ foo: 'bar' })
})
test('.rejects', () => {
return expect(fetchData(true)).rejects.toMatch('error')
})
- 将函数返回的 Promise 传递给
expect - 确保测试将断言返回,如果忽略了
return,测试将在fetchData()返回的 Promise 得到决议之前完成。
Async/Await
或者你还可以在测试用例中使用 async/await 处理 Promise。
在传递给测试用例的函数前使用 async 关键字,在函数体中使用 await。
test('async/await', async () => {
const data = await fetchData()
expect(data).toEqual({ foo: 'bar' })
})
你也可以将 async/await 与 .resolves/.rejects 结合使用,无需 return 断言。
test('async/await with .resolves/.rejects', async () => {
await expect(fetchData()).resolves.toEqual({ foo: 'bar' })
await expect(fetchData(true)).rejects.toMatch('error')
})
async 和 await 实际上是上面 Promises 示例中使用的相同逻辑的语法糖,具体如何使用,取决于你觉得哪种风格能让你的测试更简单。
Timer Mocks 模拟计时器
官方文档:Timer Mocks
原生 timer 方法(即 setTimeout、setInterval 和 Date.now 等)不太适合测试环境,因为它们依赖于实时时间。
使用可以手动“推进”时间的替代物来模拟这些功能会很有帮助,它会压缩实际等待的时间,确保你的测试快速运行。
依赖于计时器的测试仍将按照顺序解析,但会更快(React 例子)。
大部分测试框架,包括 Jest、sinon 和 lolex 都允许你在测试中模拟计时器。
有些时候可能你不想要模拟计时器。例如,在你测试动画时,或是交互端对时间较为敏感的情况下(如 API 访问速率限制器)。具有计时器模拟的库允许你在每个测试/套件上启用或禁用这个功能,因此你可以明确地选择这些测试的运行方式。
基本用法
注意:Jest 默认每个测试设置了 5 秒超时,可以修改配置项
testTimeout(默认5000)。
下面的测试没有等待异步代码执行,所以测试失败:
// 测试异步代码
function fetchData() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve({ foo: 'bar' })
}, 10000)
})
}
test('timer mock', () => {
// 必须执行一个断言
expect.assertions(1)
fetchData().then(data => {
expect(data).toEqual({ foo: 'bar' })
})
})
可以使用 done 或 async/await 等方式处理,但是需要等待至少10秒才会测试完成,如果使用模拟计时器,可以加快计时时间:
// 使用假的计时器
jest.useFakeTimers()
test('timer mock', () => {
// 必须执行一个断言
expect.assertions(1)
fetchData().then(data => {
expect(data).toEqual({ foo: 'bar' })
})
// 立即执行所有宏任务队列和微任务队列
jest.runAllTimers()
})
通过调用 jest.useFakeTimers() 启用假的计时器,它模拟了 setTimeout 和其他计时器函数。可以通过调用 jest.useRealTimers() 恢复默认的计时器行为。
开启假的计时器后,通过调用 jest.runAllTimers() 立即执行所有宏任务队列和微任务队列。
**注意:**使用了模拟计时器后,处理使用了计时器的异步代码可以不使用
done实现等待的目的,即便使用也没问题。但是不能使用 Promises 方式去处理,这样模拟计时器将被忽略,测试用例会等待真实的时间才会完成。
运行挂起的计时器
还有一些情况下,您可能会有一个递归的计时器(不同于 setInterval),在自己的计时器回调中设置新的计时器,例如:
// 测试异步代码
function fetchData() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve({ foo: 'bar' })
// 循环调用
fetchData()
}, 6000)
})
}
test('timer mock', () => {
// 使用假的计时器
jest.useFakeTimers()
// 必须执行一个断言
expect.assertions(1)
fetchData().then(data => {
expect(data).toEqual({ foo: 'bar' })
})
// 立即执行所有宏任务队列和微任务队列
jest.runAllTimers()
})
对于这种情况,运行所有计时器将是一个无休止的循环,会引发以下错误:
Aborting after running 100000 timers, assuming an infinite loop!
对于此类测试,可以使用 jest.runOnlyPendingTimers() 仅执行当前挂起的宏任务:
test('timer mock', () => {
// 使用假的计时器
jest.useFakeTimers()
// 必须执行一个断言
expect.assertions(1)
fetchData().then(data => {
expect(data).toEqual({ foo: 'bar' })
})
// 仅立即执行当前挂起的宏任务
jest.runOnlyPendingTimers()
})
如果有任何当前挂起的宏任务调度新宏任务,则此调用不会执行这些新任务。
按时间加快计时
jest.runAllTimers() 和 jest.runOnlyPendingTimers() 是立即执行任务,相当于把计时器直接快进到结束。
你也可以通过调用 jest.advanceTimersByTime(msToRun) 快进指定的时间,所有在这个时间内入列的宏任务都将被执行,如果这些宏任务中在同一时间内入列了新的宏任务,则也会执行这些新的宏任务。直到队列中没有这个时间内入列的宏任务。
注意:
jest.advanceTimersByTime(msToRun)仅执行宏任务队列。
// 测试异步代码
function fetchData() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve({ foo: 'bar' })
// 循环调用
fetchData()
}, 6000)
})
}
test('timer mock', () => {
// 使用假的计时器
jest.useFakeTimers()
// 必须执行一个断言
expect.assertions(1)
fetchData().then(data => {
expect(data).toEqual({ foo: 'bar' })
})
// 仅立即执行指定时间内入列的宏任务
jest.advanceTimersByTime(7000)
})
也可以多次调用,Jest 会将时间累积:
test('timer mock', () => {
// 使用假的计时器
jest.useFakeTimers()
// 必须执行一个断言
expect.assertions(1)
fetchData().then(data => {
expect(data).toEqual({ foo: 'bar' })
})
// 此行之前定义的断言,会立即执行 8 秒内入列的宏任务
jest.advanceTimersByTime(4000)
// 可以在中间增加断言,但仅会执行 4 秒内入列的宏任务
jest.advanceTimersByTime(4000)
})
计时器泄露
你可以在任意位置调用 jest.useFakeTimers() 或 jest.useRealTimers()(顶层、或 test(it) 块等),这是一个全局的操作,将影响同一文件中的其他测试用例。
例如:
// 测试异步代码
function fetchData() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve({ foo: 'bar' })
}, 6000)
})
}
test('timer mock1', () => {
// 使用模拟的计时器
jest.useFakeTimers()
// 必须执行一个断言
expect.assertions(1)
fetchData().then(data => {
expect(data).toEqual({ foo: 'bar' })
})
// 执行 4 秒内入列的宏任务,所以当前测试用例会失败
jest.advanceTimersByTime(4000)
})
test('timer mock2', () => {
// 此时仍在使用模拟计时器
// 必须执行一个断言
expect.assertions(1)
fetchData().then(data => {
expect(data).toEqual({ foo: 'bar' })
})
// 计时器已经经过上面的测试用例加快了4秒,这里又加快4秒,所以会执行8秒内入列的宏任务
jest.advanceTimersByTime(4000)
})
如果你计划在所有测试中不使用假计时器,则需要手动清理,否则假计时器将在测试中泄露:
afterEach(() => {
jest.useRealTimers()
})
test('do something with fake timers', () => {
jest.useFakeTimers()
// ...
})
test('do something with real timers', () => {
// ...
})
总结
// 使用模拟定时器
jest.useFakeTimers()
// 验证定时器函数被调用的次数
expect(setTimeout).toHaveBeenCalledTimes(1)
// 验证定时器的时间是 1s
expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), 1000)
// 快进所有定时器结束
jest.runAllTimers()
// 解决定时器循环问题
jest.runOnlyPendingTimers()
// 快进定时器到指定时间
jest.advanceTimersByTime(1000)
// 清除所有定时器
jest.clearAllTimers()
本文详细介绍了Jest测试框架的API,包括test函数的各种形式,常用断言如toBe、toEqual及其区别,以及异步测试的处理方法,如callback、Promise和async/await。同时,讲解了模拟计时器的使用,如jest.useFakeTimers、jest.runAllTimers等,以及如何处理计时器循环和泄露问题。内容涵盖了Jest测试的基础和进阶技巧。
7574

被折叠的 条评论
为什么被折叠?



