Skip to content

测试快照

TIP

For a beginner-friendly introduction to snapshot testing, see the Snapshot Testing tutorial.

通过 Vue School 的视频学习快照

当你希望确保函数的输出不会意外更改时,快照测试是一个非常有用的工具。

使用快照时,Vitest 将获取给定值的快照,将其比较时将参考存储在测试旁边的快照文件。如果两个快照不匹配,则测试将失败:要么更改是意外的,要么参考快照需要更新到测试结果的新版本。

使用快照

要将一个值快照,你可以使用 expect()toMatchSnapshot() API:

ts
import { expect, it } from 'vitest'

it('toUpperCase', () => {
  const result = toUpperCase('foobar')
  expect(result).toMatchSnapshot()
})

此测试在第一次运行时,Vitest 会创建一个快照文件,如下所示:

js
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html

exports['toUpperCase 1'] = '"FOOBAR"'

快照文件应该与代码更改一起提交,并作为代码审查过程的一部分进行审查。在随后的测试运行中,Vitest 会将执行的输出与之前的快照进行比较。如果他们匹配,测试就会通过。如果它们不匹配,要么测试运行时在你的代码中发现了应该修复的错误,要么实现已经更改,需要更新快照。

内联快照

Vitest stores a serialized representation of the received value. Snapshot rendering is powered by @vitest/pretty-format. snapshotFormat allows configuring general snapshot formatting behavior in Vitest. For further customization, you can implement your own custom serializers or custom snapshot matchers.

WARNING

在异步并发测试中使用快照时,由于 JavaScript 的限制,你需要使用 测试环境 中的 expect 来确保检测到正确的测试。

同样,你可以使用 toMatchInlineSnapshot() 将内联快照存储在测试文件中。

ts
import { expect, it } from 'vitest'

it('toUpperCase', () => {
  const result = toUpperCase('foobar')
  expect(result).toMatchInlineSnapshot()
})

Vitest 不会创建快照文件,而是直接修改测试文件,将快照作为字符串更新到文件中:

ts
import { expect, it } from 'vitest'

it('toUpperCase', () => {
  const result = toUpperCase('foobar')
  expect(result).toMatchInlineSnapshot('"FOOBAR"')
})

这允许你直接查看期望输出,而无需跨不同的文件跳转。

更新快照

WARNING

在异步并发测试中使用快照时,由于 JavaScript 的限制,你需要使用 测试环境 中的 expect 来确保检测到正确的测试。

当接收到的值与快照不匹配时,测试将失败,并显示它们之间的差异。当需要更改快照时,你可能希望从当前状态更新快照。

在监听(watch)模式下, 你可以在终端中键入 u 键直接更新失败的快照。

或者,你可以在 CLI 中使用 --update-u 标记使 Vitest 进入快照更新模式。

bash
vitest -u

CI behavior

By default, Vitest does not write snapshots in CI (process.env.CI is truthy) and any snapshot mismatches, missing snapshots, and obsolete snapshots fail the run. See update for the details.

An obsolete snapshot is a snapshot entry (or snapshot file) that no longer matches any collected test. This usually happens after removing or renaming tests.

文件快照

调用 toMatchSnapshot() 时,我们将所有快照存储在格式化的快照文件中。这意味着我们需要转义快照字符串中的一些字符(即双引号 " 和反引号 `)。同时,你可能会丢失快照内容的语法突出显示(如果它们是某种语言)。

为了改善这种情况,我们引入 toMatchFileSnapshot() 以在文件中显式快照。这允许你为快照文件分配任何文件扩展名,并使它们更具可读性。

ts
import { expect, it } from 'vitest'

it('render basic', async () => {
  const result = renderHTML(h('div', { class: 'foo' }))
  await expect(result).toMatchFileSnapshot('./test/basic.output.html')
})

它将与 ./test/basic.output.html 的内容进行比较。并且可以用 --update 标志写回。

图像快照

对于 UI 组件和页面的视觉回归测试,Vitest 通过 浏览器模式 提供了内置支持,使用 toMatchScreenshot() 断言:

ts
import { expect, test } from 'vitest'
import { page } from 'vitest/browser'

test('button looks correct', async () => {
  const button = page.getByRole('button')
  await expect(button).toMatchScreenshot('primary-button')
})

它会捕获屏幕截图并与参考图像进行比较,以检测意外的视觉变化。在 视觉回归测试指南中了解更多内容。

自定义序列化器

你可以添加自己的逻辑来修改快照的序列化方式。像 Jest 一样,Vitest 默认有内置的 JavaScript 类型、HTML 元素、ImmutableJS 和 React 元素提供了默认的序列化程序。

可以使用 expect.addSnapshotSerializer 添加自定义序列器。

ts
expect.addSnapshotSerializer({
  serialize(val, config, indentation, depth, refs, printer) {
    // `printer` 是一个通过现有插件对值进行序列化的函数。
    return `Pretty foo: ${printer(
      val.foo,
      config,
      indentation,
      depth,
      refs
    )}`
  },
  test(val) {
    return val && Object.prototype.hasOwnProperty.call(val, 'foo')
  },
})

我们还支持 snapshotSerializers 选项,可以隐式添加自定义序列化器。

path/to/custom-serializer.ts
ts
import { SnapshotSerializer } from 'vitest'

export default {
  serialize(val, config, indentation, depth, refs, printer) {
    // `printer` 是一个使用现有插件序列化数值的函数。
    return `Pretty foo: ${printer(val.foo, config, indentation, depth, refs)}`
  },
  test(val) {
    return val && Object.prototype.hasOwnProperty.call(val, 'foo')
  },
} satisfies SnapshotSerializer
vitest.config.ts
ts
import { defineConfig } from 'vitest/config'

export default defineConfig({
  test: {
    snapshotSerializers: ['path/to/custom-serializer.ts'],
  },
})

添加类似的测试后:

ts
test('foo snapshot test', () => {
  const bar = {
    foo: {
      x: 1,
      y: 2,
    },
  }

  expect(bar).toMatchSnapshot()
})

你将获得以下快照:

Pretty foo: Object {
  "x": 1,
  "y": 2,
}

Custom Snapshot Matchers experimental 4.1.3+

You can build custom snapshot matchers using the composable functions exposed on Snapshots from vitest. These let you transform values before snapshotting while preserving full snapshot lifecycle support (creation, update, inline rewriting).

ts
import { expect, test, Snapshots } from 'vitest'

const { toMatchFileSnapshot, toMatchInlineSnapshot, toMatchSnapshot } = Snapshots

expect.extend({
  toMatchTrimmedSnapshot(received: string, length: number) {
    return toMatchSnapshot.call(this, received.slice(0, length))
  },
  toMatchTrimmedInlineSnapshot(received: string, inlineSnapshot?: string) {
    return toMatchInlineSnapshot.call(this, received.slice(0, 10), inlineSnapshot)
  },
  async toMatchTrimmedFileSnapshot(received: string, file: string) {
    return toMatchFileSnapshot.call(this, received.slice(0, 10), file)
  },
})

test('file snapshot', () => {
  expect('extra long string oh my gerd').toMatchTrimmedSnapshot(10)
})

test('inline snapshot', () => {
  expect('extra long string oh my gerd').toMatchTrimmedInlineSnapshot()
})

test('raw file snapshot', async () => {
  await expect('extra long string oh my gerd').toMatchTrimmedFileSnapshot('./raw-file.txt')
})

The composables return { pass, message } so you can further customize the error:

ts
import { Snapshots } from 'vitest'

const { toMatchSnapshot } = Snapshots

expect.extend({
  toMatchTrimmedSnapshot(received: string, length: number) {
    const result = toMatchSnapshot.call(this, received.slice(0, length))
    return { ...result, message: () => `Trimmed snapshot failed: ${result.message()}` }
  },
})

WARNING

For inline snapshot matchers, the snapshot argument must be the last parameter (or second-to-last when using property matchers). Vitest rewrites the last string argument in the source code, so custom arguments before the snapshot work, but custom arguments after it are not supported.

TIP

File snapshot matchers must be asynctoMatchFileSnapshot returns a Promise. Remember to await the result in the matcher and in your test.

For TypeScript, extend the Assertion interface:

ts
import 'vitest'

declare module 'vitest' {
  interface Assertion<T = any> {
    toMatchTrimmedSnapshot: (length: number) => T
    toMatchTrimmedInlineSnapshot: (inlineSnapshot?: string) => T
    toMatchTrimmedFileSnapshot: (file: string) => Promise<T>
  }
}

TIP

See Extending Matchers for more on expect.extend and custom matcher conventions.

与 Jest 的区别

Vitest 提供了与 Jest 几乎兼容的快照功能,除少数例外:

1. 快照文件中的注释标头不同

diff
- // Jest Snapshot v1, https://goo.gl/fbAQLP
+ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html

这实际上不会影响功能,但在从 Jest 迁移时可能会影响提交差异。

2. printBasicPrototype 默认为 false

Jest 和 Vitest的快照功能均基于 pretty-format 实现,但 Vitest 在 @vitest/pretty-format 基础上应用了自定义的快照默认配置。具体而言,Vitest将 printBasicPrototype 设为 false 以生成更简洁的快照输出,而 Jest 29.0.0 以下版本默认将该值设为 true

ts
import { expect, test } from 'vitest'

test('snapshot', () => {
  const bar = [
    {
      foo: 'bar',
    },
  ]

  // 在 Jest 中的输出格式
  expect(bar).toMatchInlineSnapshot(`
    Array [
      Object {
        "foo": "bar",
      },
    ]
  `)

  // 在 Vitest 中的输出格式
  expect(bar).toMatchInlineSnapshot(`
    [
      {
        "foo": "bar",
      },
    ]
  `)
})

我们相信这种预设有更好的可读性和开发体验。如果你仍然喜欢 Jest 的行为,可以通过以下方式更改配置:

vitest.config.ts
ts
import { defineConfig } from 'vitest/config'

export default defineConfig({
  test: {
    snapshotFormat: {
      printBasicPrototype: true,
    },
  },
})

3. 使用 V 形 > 而非冒号 : 作为自定义消息的分隔符

当创建快照文件期间传递自定义消息时,Vitest 使用 V 形 > 作为分隔符而不是冒号 : 以提高自定义消息可读性。

对于以下示例测试代码:

js
test('toThrowErrorMatchingSnapshot', () => {
  expect(() => {
    throw new Error('error')
  }).toThrowErrorMatchingSnapshot('hint')
})

在 Jest 中,快照将是:

console
exports[`toThrowErrorMatchingSnapshot: hint 1`] = `"error"`;

在 Vitest 中,等效的快照将是:

console
exports[`toThrowErrorMatchingSnapshot > hint 1`] = `[Error: error]`;

4. toThrowErrorMatchingSnapshottoThrowErrorMatchingInlineSnapshot 的默认 Error 快照不同

js
import { expect, test } from 'vitest'

test('snapshot', () => {
  // 在 Jest 和 Vitest 中
  expect(new Error('error')).toMatchInlineSnapshot(`[Error: error]`)

  // Jest 会对 `Error` 实例的 `Error.message` 生成快照
  // Vitest 则会输出与 toMatchInlineSnapshot 相同的值
  expect(() => {
    throw new Error('error')
  }).toThrowErrorMatchingInlineSnapshot(`"error"`) 
}).toThrowErrorMatchingInlineSnapshot(`[Error: error]`) 
})