Skip to content

使用匹配器

Vitest 使用 expect 配合 “匹配器” 来断言值是否满足特定条件。本章介绍最常用的匹配器。完整列表请参阅 Expect API

常见匹配器

测试一个值时,最简单的方法是检查它是否精确相等。当你编写 expect(2 + 2).toBe(4) 时,toBe 匹配器使用 Object.is 检查值是否完全等于 4

js
import { expect, test } from 'vitest'

test('two plus two is four', () => {
  expect(2 + 2).toBe(4)
})

这种方式适用于检查数字、字符串和布尔值等原始值。但在比较对象时,toBe 检查的是 恒等性(它们是否是内存中的同一个对象),而不是它们是否具有相同的结构。这时就需要用到 toEqual。它会递归地比较对象或数组的每个字段或元素,忽略对象恒等性:

js
test('object assignment', () => {
  const data = { one: 1 }
  data.two = 2

  expect(data).toEqual({ one: 1, two: 2 })
})

下面这个例子更清楚地展示了两者之间的差异。两个内容相同的对象 toEqual 会通过,但 toBe 会失败:

js
test('toBe vs toEqual', () => {
  const a = { name: 'Alice' }
  const b = { name: 'Alice' }

  // 它们在内存中是不同的对象
  expect(a).not.toBe(b)

  // 但结构相同
  expect(a).toEqual(b)
})

还有 toStrictEqual,它在三个方面比 toEqual 更严格:检查 undefined 属性、区分稀疏数组和 undefined 值,以及验证对象是否具有相同的类型(不仅仅是相同的结构):

js
test('toEqual vs toStrictEqual', () => {
  // toEqual 忽略 undefined 属性
  expect({ a: 1 }).toEqual({ a: 1, b: undefined })

  // toStrictEqual 会捕获它们
  expect({ a: 1 }).not.toStrictEqual({ a: 1, b: undefined })

  // toEqual 不检查对象类型
  class User {
    constructor(name) {
      this.name = name
    }
  }
  expect(new User('Alice')).toEqual({ name: 'Alice' })
  expect(new User('Alice')).not.toStrictEqual({ name: 'Alice' })
})

TIP

一个好的经验法则是:对原始值类型(数字、字符串、布尔值)使用 toBe,比较结构时使用 toEqual,当你还关心类型和显式的 undefined 值时使用 toStrictEqual

你可以在任何匹配器前插入 .not 来否定它。适用于验证某些 不成立 情况:

js
test('adding positive numbers is not zero', () => {
  expect(1 + 2).not.toBe(0)
})

真值

在测试中,有时你需要区分 undefinednullfalse。其他时候你不关心确切的值,只想知道具体是真值还是假值。Vitest 为这两种情况提供了相应的匹配器:

你应该选择最能精确描述你检查内容的匹配器。当你实际意思是 toBeDefined 时使用 toBeTruthy 可能会掩盖 bug,因为 0"" 都是已定义的但却是假值。

js
test('null checks', () => {
  const n = null

  expect(n).toBeNull()
  expect(n).toBeDefined()
  expect(n).toBeFalsy()
  expect(n).not.toBeTruthy()
  expect(n).not.toBeUndefined()
})

test('zero', () => {
  const z = 0

  expect(z).toBeDefined() // 通过:0 已定义
  expect(z).toBeFalsy() // 通过:0 是假值
  expect(z).not.toBeNull() // 通过:0 不是 null
})

数字

大多数数字比较都很直接。Vitest 提供了大于、小于和相等检查的匹配器:

js
test('number comparisons', () => {
  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)
})

浮点数运算有一个常见的陷阱。在 JavaScript 中,0.1 + 0.2 并不完全等于 0.3(它是 0.30000000000000004)。这意味着 toBe(0.3) 检查会失败。请改用 toBeCloseTo,它会在较小的舍入误差范围内比较数字:

js
test('adding floating point numbers', () => {
  const value = 0.1 + 0.2

  // 由于浮点数舍入,这不会通过
  // expect(value).toBe(0.3)

  // 这个可以
  expect(value).toBeCloseTo(0.3)
})

字符串

你可以使用 toMatch 根据正则表达式测试字符串。当你关心形式而非确切值时,这特别方便,例如检查错误消息是否包含某个单词,或者 URL 是否符合特定格式:

js
test('there is no I in team', () => {
  expect('team').not.toMatch(/I/)
})

test('version string matches semver format', () => {
  expect('vitest@1.0.0').toMatch(/vitest@\d+\.\d+\.\d+/)
})

数组和可迭代对象

toContain 检查数组(或任何可迭代对象,如 Set)是否包含特定项。它使用 === 进行比较,因此对原始类型效果很好:

js
test('the shopping list has milk in it', () => {
  const shoppingList = ['milk', 'bread', 'eggs', 'butter']

  expect(shoppingList).toContain('milk')
  expect(new Set(shoppingList)).toContain('milk')
})

如果你需要检查数组是否包含具有特定结构的对象,请改用 toContainEqual。它的工作原理类似于 toEqual,但用于数组中的单个元素。

对象

测试对象时,你通常只想检查几个重要的字段,而不是检查每个属性。toMatchObject 正是为此而设计。它验证对象至少包含你指定的属性,并忽略任何额外的属性:

js
test('user has expected fields', () => {
  const user = {
    id: 1,
    name: 'Alice',
    email: 'alice@example.com',
    createdAt: '2024-01-01'
  }

  // 这里我们只关心 name 和 email
  expect(user).toMatchObject({
    name: 'Alice',
    email: 'alice@example.com',
  })
})

对于检查单个属性,特别是嵌套属性,toHaveProperty 更具可读性。你传递一个点分隔的路径,并可选择性地传递一个期望值:

js
test('object has property', () => {
  const user = {
    name: 'Alice',
    address: { city: 'Paris', zip: '75001' }
  }

  expect(user).toHaveProperty('name')
  expect(user).toHaveProperty('name', 'Alice')
  expect(user).toHaveProperty('address.city', 'Paris')
  expect(user).toHaveProperty('address.zip')
})

非对称匹配器

有时你不知道确切的值,但知道它的类型或结构。非对称匹配器让你可以描述值应该 看起来应该是什么样,而无需确定确切内容。它们可以在任何进行深度比较的匹配器内部工作,例如 toEqualtoMatchObject

js
test('user has the right shape', () => {
  const user = createUser('Alice')

  expect(user).toEqual({
    id: expect.any(Number),
    name: 'Alice',
    email: expect.stringContaining('@'),
    roles: expect.arrayContaining(['viewer']),
  })
})

最常用的非对称匹配器有:

异常

要验证函数是否抛出错误,请使用 toThrow。你需要将调用包装在另一个函数中,以便 Vitest 可以捕获错误而不是让它导致测试崩溃:

js
function compileCode(code) {
  if (code === '') {
    throw new Error('Cannot compile empty string')
  }
  return code
}

test('compiling an empty string throws', () => {
  // 检查它是否抛出异常
  expect(() => compileCode('')).toThrow()

  // 检查错误信息
  expect(() => compileCode('')).toThrow('Cannot compile empty string')

  // 使用正则表达式检查错误信息
  expect(() => compileCode('')).toThrow(/empty string/)
})

TIP

包装函数 () => compileCode('') 很重要。如果你写成 expect(compileCode('')).toThrow(),错误会在 expect 有机会捕获它 之前 就被抛出,测试将因未处理的错误而失败。

软断言

通常,失败的断言会立即停止测试。这适用大多数情况,但有时你想检查多个独立项并一次性看到所有失败,而不是逐个修复。

expect.soft 正是为此而设计。它会记录失败,但让测试继续运行:

js
test('check multiple fields', () => {
  const user = { name: 'Alice', age: 30, role: 'admin' }

  expect.soft(user.name).toBe('Alice')
  expect.soft(user.age).toBe(25) // 这个会失败,但执行会继续
  expect.soft(user.role).toBe('admin')
  // 测试报告将显示 age 不匹配
})

这对于验证 API 响应或复杂对象的结构特别有用,因为多个字段可能同时出错。