我使用 Jest 时“哇,我没想到!”的时刻
Jest一直是我最常用的单元测试工具。它非常强大,我开始怀疑自己是不是一直没能充分利用它。虽然测试都通过了,但随着时间的推移,我还是会不时地重构测试,因为我不知道 Jest 居然能做到这一点。每次我查看 Jest 文档,代码总是不一样。
因此,我将分享一些我最喜欢的 Jest 技巧,你们中的一些人可能已经知道了,因为你们没有像我一样跳过阅读文档(真丢脸),但我希望这对那些跳过阅读文档的人有所帮助!
仅供参考,我使用的是 Jest v24.8.0,因此如果您当前使用的 Jest 版本存在某些功能无法正常工作,请务必注意。另外,这些示例并非实际的测试代码,仅供演示。
#1..toBe
对比.toEqual
乍一看,这些断言对我来说都很好:
expect('foo').toEqual('foo')
expect(1).toEqual(1)
expect(['foo']).toEqual(['foo'])
之前用chai做过相等断言(to.equal
),所以这很自然。事实上,Jest 不会报错,这些断言照常通过。
然而,Jest 有.toBe
和.toEqual
。前者用于使用 断言相等性Object.is
,而后者用于对对象和数组进行深度相等性断言。现在,如果事实证明不需要深度相等性(例如对原始值进行相等性断言),.toEqual
有一个后备方案可供使用Object.is
,这也解释了为什么之前的示例能够顺利通过。
expect('foo').toBe('foo')
expect(1).toBe(1)
expect(['foo']).toEqual(['foo'])
因此,如果您已经知道要测试什么类型的值,则可以.toEqual
跳过所有 if-else 。.toBe
一个常见的错误是,您会使用它.toBe
来断言非原始值的相等性。
expect(['foo']).toBe(['foo'])
如果你查看源代码,当.toBe
失败时,它会尝试检查你是否确实通过调用使用的函数.toEqual
犯了该错误。这可能会成为优化测试时的瓶颈。
如果您确定要断言原始值,则可以重构代码以达到优化目的:
expect(Object.is('foo', 'foo')).toBe(true)
查看文档中的更多详细信息。
#2. 更合适的匹配者
从技术上讲,你可以用它.toBe
来断言任何类型的值。使用 Jest,你可以专门使用某些匹配器,从而使你的测试更具可读性(在某些情况下,可以更短)。
// 🤔
expect([1,2,3].length).toBe(3)
// 😎
expect([1,2,3]).toHaveLength(3)
const canBeUndefined = foo()
// 🤔
expect(typeof canBeUndefined !== 'undefined').toBe(true)
// 🤔
expect(typeof canBeUndefined).not.toBe('undefined')
// 🤔
expect(canBeUndefined).not.toBe(undefined)
// 😎
expect(canBeUndefined).toBeDefined()
class Foo {
constructor(param) {
this.param = param
}
}
// 🤔
expect(new Foo('bar') instanceof Foo).toBe(true)
// 😎
expect(new Foo('bar')).toBeInstanceOf(Foo)
这些只是我从文档中长长的 Jest 匹配器列表中挑选出来的几个,你可以查看其余的。
#3. 对非 UI 元素进行快照测试
你可能听说过Jest 中的快照测试,它可以帮助你监控 UI 元素的变化。但快照测试并不仅限于此。
考虑这个例子:
const allEmployees = getEmployees()
const happyEmployees = giveIncrementByPosition(allEmployees)
expect(happyEmployees[0].nextMonthPaycheck).toBe(1000)
expect(happyEmployees[1].nextMonthPaycheck).toBe(5000)
expect(happyEmployees[2].nextMonthPaycheck).toBe(4000)
// ...etc
如果必须对越来越多的员工进行断言,那将会非常繁琐。此外,如果发现每个员工都需要进行更多断言,那么将新断言的数量乘以员工数量即可。
通过快照测试,所有这些都可以简单地完成:
const allEmployees = getEmployees()
const happyEmployees = giveIncrementByPosition(allEmployees)
expect(happyEmployees).toMatchSnapshot()
每当出现回归时,您就会确切地知道节点中的哪棵树与快照不匹配。
然而,这种便捷性是有代价的:它更容易出错。你很可能根本不知道快照实际上是错的,最终还是会提交它。所以,一定要仔细检查你的快照,就像它是你自己的断言代码一样(因为它确实是)。
当然,快照测试还有更多内容。查看完整文档。
#4.describe.each
和test.each
您是否写过一些与此类似的测试?
describe('When I am a supervisor', () => {
test('I should have a supervisor badge', () => {
const employee = new Employee({ level: 'supervisor' })
expect(employee.badges).toContain('badge-supervisor')
})
test('I should have a supervisor level', () => {
const employee = new Employee({ level: 'supervisor' })
expect(employee.level).toBe('supervisor')
})
})
describe('When I am a manager', () => {
test('I should have a manager badge', () => {
const employee = new Employee({ level: 'manager' })
expect(employee.badges).toContain('badge-manager')
})
test('I should have a manager level', () => {
const employee = new Employee({ level: 'manager' })
expect(employee.level).toBe('manager')
})
})
这太重复了,对吧?想象一下,如果处理更多案例,情况会怎样?
使用describe.each
和test.each
,你可以将代码压缩如下:
const levels = [['manager'], ['supervisor']]
const privileges = [['badges', 'toContain', 'badge-'], ['level', 'toBe', '']]
describe.each(levels)('When I am a %s', (level) => {
test.each(privileges)(`I should have a ${level} %s`, (kind, assert, prefix) => {
const employee = new Employee({ level })
expect(employee[kind])[assert](`${prefix}${level}`)
})
})
然而,我还没有在自己的测试中真正使用它,因为我更喜欢详细的测试,但我只是认为这是一个有趣的技巧。
查看文档以获取有关参数的更多详细信息(剧透:表格语法真的很酷)。
#5. 模拟一次全局函数
有时候,你需要测试一些依赖于特定测试用例的全局函数。例如,一个使用 JavaScript 对象获取当前日期信息的函数Date
,或者一个依赖该对象的库。棘手的是,如果信息与当前日期有关,你永远无法得到正确的断言。
function foo () {
return Date.now()
}
expect(foo()).toBe(Date.now())
// ❌ This would throw occasionally:
// expect(received).toBe(expected) // Object.is equality
//
// Expected: 1558881400838
// Received: 1558881400837
最终,您必须覆盖Date
全局对象,以使其保持一致且可控:
function foo () {
return Date.now()
}
Date.now = () => 1234567890123
expect(foo()).toBe(1234567890123) // ✅
然而,这被认为是一种不好的做法,因为覆盖在测试之间仍然存在。如果没有其他测试依赖于Date.now
,你不会注意到它,但它确实存在泄漏。
test('First test', () => {
function foo () {
return Date.now()
}
Date.now = () => 1234567890123
expect(foo()).toBe(1234567890123) // ✅
})
test('Second test', () => {
function foo () {
return Date.now()
}
expect(foo()).not.toBe(1234567890123) // ❌ ???
})
我曾经以一种不会泄漏的方式对其进行“破解”:
test('First test', () => {
function foo () {
return Date.now()
}
const oriDateNow = Date.now
Date.now = () => 1234567890123
expect(foo()).toBe(1234567890123) // ✅
Date.now = oriDateNow
})
test('Second test', () => {
function foo () {
return Date.now()
}
expect(foo()).not.toBe(1234567890123) // ✅ as expected
})
然而,有一个更好、更简单的方法可以做到这一点:
test('First test', () => {
function foo () {
return Date.now()
}
jest.spyOn(Date, 'now').mockImplementationOnce(() => 1234567890123)
expect(foo()).toBe(1234567890123) // ✅
})
test('Second test', () => {
function foo () {
return Date.now()
}
expect(foo()).not.toBe(1234567890123) // ✅ as expected
})
总而言之,jest.spyOn
监视全局Date
对象并模拟now
函数的一次调用。这将Date.now
在其余测试中保持不变。
关于 Jest 中的 mocking 还有很多内容。请查看完整文档了解更多详情。
这篇文章越来越长了,所以暂时就到这里。以上只是 Jest 功能的冰山一角,我只是重点介绍了我最喜欢的几个功能。如果你还有其他有趣的内容,也请告诉我。
另外,如果你经常使用 Jest,可以试试Majestic,它是 Jest 的零配置 GUI,能让你摆脱枯燥的终端输出。我不确定它的作者是否在 dev.to,不过还是想跟他打个招呼。
一如往常,感谢您阅读我的帖子!
封面图片来自https://jestjs.io/
文章来源:https://dev.to/briwa/my-whoa-i-didn-t-know-that-moments-with-jest-3a3h