我使用 Jest 时“哇,我没想到!”的时刻

2025-05-24

我使用 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'])
Enter fullscreen mode Exit fullscreen mode

之前用chai做过相等断言(to.equal),所以这很自然。事实上,Jest 不会报错,这些断言照常通过。

然而,Jest 有.toBe.toEqual。前者用于使用 断言相等性Object.is,而后者用于对对象和数组进行深度相等性断言。现在,如果事实证明不需要深度相等性(例如对原始值进行相等性断言),.toEqual有一个后备方案可供使用Object.is,这也解释了为什么之前的示例能够顺利通过。

expect('foo').toBe('foo')
expect(1).toBe(1)
expect(['foo']).toEqual(['foo'])
Enter fullscreen mode Exit fullscreen mode

因此,如果您已经知道要测试什么类型的值,则可以.toEqual跳过所有 if-else 。.toBe

一个常见的错误是,您会使用它.toBe来断言非原始值的相等性。

expect(['foo']).toBe(['foo'])
Enter fullscreen mode Exit fullscreen mode

如果你查看源代码,当.toBe失败时,它会尝试检查你是否确实通过调用使用的函数.toEqual犯了该错误。这可能会成为优化测试时的瓶颈。

如果您确定要断言原始值,则可以重构代码以达到优化目的:

expect(Object.is('foo', 'foo')).toBe(true)
Enter fullscreen mode Exit fullscreen mode

查看文档中的更多详细信息。

#2. 更合适的匹配者

从技术上讲,你可以用它.toBe来断言任何类型的值。使用 Jest,你可以专门使用某些匹配器,从而使你的测试更具可读性(在某些情况下,可以更短)。

// 🤔
expect([1,2,3].length).toBe(3)

// 😎
expect([1,2,3]).toHaveLength(3)
Enter fullscreen mode Exit fullscreen mode
const canBeUndefined = foo()

// 🤔
expect(typeof canBeUndefined !== 'undefined').toBe(true)

// 🤔
expect(typeof canBeUndefined).not.toBe('undefined')

// 🤔
expect(canBeUndefined).not.toBe(undefined)

// 😎
expect(canBeUndefined).toBeDefined()
Enter fullscreen mode Exit fullscreen mode
class Foo {
  constructor(param) {
    this.param = param
  }
}

// 🤔
expect(new Foo('bar') instanceof Foo).toBe(true)

// 😎
expect(new Foo('bar')).toBeInstanceOf(Foo)
Enter fullscreen mode Exit fullscreen mode

这些只是我从文档中长长的 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
Enter fullscreen mode Exit fullscreen mode

如果必须对越来越多的员工进行断言,那将会非常繁琐。此外,如果发现每个员工都需要进行更多断言,那么将新断言的数量乘以员工数量即可。

通过快照测试,所有这些都可以简单地完成:

const allEmployees = getEmployees()
const happyEmployees = giveIncrementByPosition(allEmployees)

expect(happyEmployees).toMatchSnapshot()
Enter fullscreen mode Exit fullscreen mode

每当出现回归时,您就会确切地知道节点中的哪棵树与快照不匹配。

然而,这种便捷性是有代价的:它更容易出错。你很可能根本不知道快照实际上是错的,最终还是会提交它。所以,一定要仔细检查你的快照,就像它是你自己的断言代码一样(因为它确实是)。

当然,快照测试还有更多内容。查看完整文档

#4.describe.eachtest.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')
  })
})
Enter fullscreen mode Exit fullscreen mode

这太重复了,对吧?想象一下,如果处理更多案例,情况会怎样?

使用describe.eachtest.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}`)
  })
})
Enter fullscreen mode Exit fullscreen mode

然而,我还没有在自己的测试中真正使用它,因为我更喜欢详细的测试,但我只是认为这是一个有趣的技巧。

查看文档以获取有关参数的更多详细信息(剧透:表格语法真的很酷)。

#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
Enter fullscreen mode Exit fullscreen mode

最终,您必须覆盖Date全局对象,以使其保持一致且可控:

function foo () {
  return Date.now()
}

Date.now = () => 1234567890123

expect(foo()).toBe(1234567890123) // ✅
Enter fullscreen mode Exit fullscreen mode

然而,这被认为是一种不好的做法,因为覆盖在测试之间仍然存在。如果没有其他测试依赖于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) // ❌ ???
})
Enter fullscreen mode Exit fullscreen mode

我曾经以一种不会泄漏的方式对其进行“破解”:

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
})
Enter fullscreen mode Exit fullscreen mode

然而,有一个更好、更简单的方法可以做到这一点:

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
})
Enter fullscreen mode Exit fullscreen mode

总而言之,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
PREV
免费编程书籍(更新)
NEXT
最强大的 React JS 速查表