使用匹配器
Vitest 使用 expect 搭配「匹配器」来断言值是否符合特定条件。本页面涵盖了最常用的匹配器。完整列表请参考 Expect API 参考。
常用匹配器
测试值最简单的方式是使用严格相等。当编写 expect(2 + 2).toBe(4) 时,toBe 匹配器会使用 Object.is 检查该值是否精确等于 4。
import { expect, test } from 'vitest'
test('two plus two is four', () => {
expect(2 + 2).toBe(4)
})这对数字、字符串、布尔值等原始类型非常有效。但当你比较对象时,toBe 检查的是引用(它们是否是内存中完全相同的对象),而不是结构是否相同。这时就需要使用 toEqual:它会递归比较对象或数组的每一个字段,忽略对象引用:
test('object assignment', () => {
const data = { one: 1 }
data.two = 2
expect(data).toEqual({ one: 1, two: 2 })
})下面是一个更清晰的例子,展示相同内容的两个对象在 toBe 和 toEqual 下的不同表现:
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 值,并验证对象具有相同的类型(而不只是相同的结构):
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 来否定任何匹配器。这在需要验证某件事不成立时很有用:
test('adding positive numbers is not zero', () => {
expect(1 + 2).not.toBe(0)
})真值判断
在测试中,你有时需要区分 undefined、null 和 false。有时你并不关心具体值,只想知道某个值是真值还是假值。Vitest 提供了对应的匹配器:
toBeNull只匹配nulltoBeUndefined只匹配undefinedtoBeDefined是toBeUndefined的反义词。只要不是undefined就通过toBeTruthy匹配if语句中视为真的任何值toBeFalsy匹配if语句中视为假的任何值
你应该选择最能准确描述你检查内容的匹配器。在确实意味着 toBeDefined 时使用 toBeTruthy 可能会隐藏 bug,因为 0 和 "" 都有定义但属于假值。
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 提供你期望的用于大于、小于和相等检查的匹配器:
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 来比较在微小舍入误差内的数字:
test('adding floating point numbers', () => {
const value = 0.1 + 0.2
// 这不会生效,因为浮点数舍入
// expect(value).toBe(0.3)
// 这样可以
expect(value).toBeCloseTo(0.3)
})字符串
你可以使用 toMatch 对字符串进行正则表达式匹配。这在你不关心精确值,而只关心某种模式时非常有用,例如检查错误消息是否包含某个单词,或 URL 是否符合特定格式:
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)是否包含某个特定项。它使用 === 进行比较,因此对原始类型非常有效:
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 可以做到这一点。它会验证对象至少包含你指定的属性,并忽略其他属性:
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 更易读。你可以传入一个点分隔的路径,并可选地传入预期值:
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')
})非对称匹配器
有时你不知道确切的值,但你知道它的类型或结构。非对称匹配器可以让你描述一个值应该“看起来像什么”,而不必锁定具体内容。它们可以在任何进行深度比较的匹配器中使用,比如 toEqual 或 toMatchObject:
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']),
})
})最常见的非对称匹配器有:
expect.any(Constructor)匹配使用给定构造函数创建的任何值(例如Number、String、Array)expect.stringContaining(str)匹配包含给定子字符串的字符串expect.stringMatching(regex)将字符串与正则表达式进行匹配expect.arrayContaining(arr)匹配包含预期数组中所有项的数组(顺序无关,允许额外项)expect.objectContaining(obj)匹配至少包含指定属性的对象
异常
要验证函数是否抛出错误,请使用 toThrow。你需要将调用包装在另一个函数中,以便 Vitest 能捕获错误,而不是让错误导致测试崩溃:
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 正是为此而设计的。它记录失败但允许测试继续执行:
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 响应或复杂对象的结构时特别有用,因为多个字段可能同时出错。
