Vitest 简介
由 Vite 提供支持的极速原生的单元测试框架。安装 pnpm add -D vitest,Vitest 需要 Vite >=v3.0.0 和 Node >=v14。
- Vite 支持 
 重复使用 Vite 的配置、转换器、解析器和插件 - 在您的应用程序和测试中保持一致。
- 兼容 Jest 
 拥有预期、快照、覆盖等 - 从 Jest 迁移很简单。
- 智能即时浏览模式 
 智能文件监听模式,就像是测试的 HMR!
- ESM, TypeScript, JSX 
 由 esbuild 提供的开箱即用 ESM、TypeScript 和 JSX 支持。
Vitest 跟其他的测试框架进行对比
Vitest 和基于浏览器的运行器之间的主要区别是速度和执行上下文。简而言之,基于浏览器的运行器,如 Cypress,可以捕捉到基于 Node 的运行器(如 Vitest)所不能捕捉的问题(比如样式问题、原生 DOM 事件、Cookies、本地存储和网络故障),但基于浏览器的运行器比 Vitest 慢几个数量级,因为它们要执行打开浏览器,编译样式表以及其他步骤。
Jest
Jest 通过为大多数的 JavaScript 项目提供了开箱即用的测试支持,填补了测试框架的空白,有着舒适的 API(例如 it 和 expect),以及大多数所需要的全套测试功能(例如快照,对象模拟,代码测试覆盖率)。
如果你的项目由 Vite 驱动,Jest 和 Vite 之间有很多重复的部分,让用户不得不创建两个不同的配置文件。配置和维护两个不同的容器是一件极其不合理的操作。使用 Vitest,你就可以将开发,构建和测试环境的配置定义为单个容器,共享相同的插件和 vite.config.js。同时可以通过相同的插件 API 进行扩展,与 Vite 形成完美的集成。
由于 Jest 的使用规模,Vitest 提供了与之兼容的 API,允许在大多数项目中将其作为备选使用,同时还包括了单元测试时最常见的功能(模拟,快照以及覆盖率)。Vitest 与大多数 Jest API 和生态系统库都有较好的兼容性,因此在大多数项目中,可以无缝的将 Jest 替换成 Vitest 。
Cypress
Cypress 是著名的端到端测试工具,是基于浏览器的测试工具,是 Vitest 的补充工具之一。
基于浏览器运行测试的框架(Cypress、Web Test),会捕获到 Vitest 无法捕获的问题,因为他们都是使用真实的浏览器和 APIs。相比之下,Vitest 专注于为非浏览器逻辑提供最佳的开发体验。如果你想使用 Cypress,建议将 Vitest 用于测试项目中非浏览器逻辑,将 Cypress 用于测试依赖浏览器的逻辑。
Cypress 更像是一个 IDE 而不是测试框架,因为您还可以在浏览器中看到真实呈现的组件,以及它的测试结果和日志。Cypress 还尝试将 Vite 集成进他们自己的产品中:使用 Vitesse 重新构建他们的应用程序的 UI,并使用 Vite 来测试驱动他们项目的开发。
Vitest 支持各种实现部分浏览器环境的第三方包,例如 jsdom,可以让我们快速的对于任何引用浏览器 APIs 的代码进行单元测试。但这些浏览器环境在实现上有局限性,例如 jsdom 缺少相当数量的特性,诸如 window.navigation 或者布局引擎(offsetTop 等)。
Cypress 不是对业务代码进行单元测试的好选择,但使用 Cypress(用于端对端和组件测试)配合 Vitest(用于非浏览器逻辑的单元测试)将满足你应用程序的测试需求。
Vitest 主要功能
- 与 Vite 通用的配置、转换器、解析器和插件。
- 使用你的应用程序中的相同配置来进行测试!
- 智能文件监听模式(默认启用),就像是测试的 HMR!
- 支持测试 Vue、React、Lit 等框架中的组件。
- 开箱即用的 ES Module / TypeScript / JSX support / PostCSS
- ESM 优先,支持模块顶级 await
- 注重性能,通过 tinypool 使用 Worker 线程尽可能多地并发运行
- 使用 Tinybench 来支持基准测试
- 套件和测试的过滤、超时、并发配置
- Jest 的快照功能
- 内置 Chai 进行断言 + 与 Jest expect 语法兼容的 API
- 内置用于对象模拟(Mock)的 Tinyspy
- 使用 jsdom 或 happy-dom 用于 DOM 模拟
- 通过 c8 来输出代码测试覆盖率
- 类似于 Rust 语言的 源码内联测试
- 通过 expect-type 进行类型测试
测试环境
Vitest 提供 environment 选项以在特定环境中运行代码,可以使用 environmentOptions 选项修改环境的行为方式。默认情况下,可以使用这些环境:
- node为默认环境
- jsdom通过提供 Browser API 模拟浏览器环境,使用- jsdom包
- happy-dom通过提供 Browser API 模拟浏览器环境,被认为比 jsdom 更快,但缺少一些 API,使用- happy-dom包
- edge-runtime模拟 Vercel 的 edge-runtime,使用- @edge-runtime/vm包
- 设置 - environment选项时,它将应用于项目中的所有测试文件。要获得更细粒度的控制,可以使用控制注释为特定文件指定环境,以- @vitest-environment开头,后跟环境名称的注释:- 1 
 2
 3
 4
 5
 6
 7- // @vitest-environment jsdom 
 import { test } from 'vitest'
 test('test', () => {
 expect(typeof window).not.toBe('undefined')
 })- 也可以通过设置 - environmentMatchGlobs选项,根据 glob 模式指定环境。
- 从 0.23.0 开始,你可以创建自己的包,名为 - vitest-environment-${name},来扩展 Vitest 环境。该包应导出一个具有- Environment属性的对象:- 1 
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13- import type { Environment } from 'vitest' 
 export default <Environment>{
 name: 'custom',
 setup() {
 // custom setup
 return {
 teardown() {
 // called after all tests with this env have been run
 },
 }
 },
 }- 可以通过 - vitest/environments访问默认的 Vitest 环境:- 1 
 2
 3- import { builtinEnvironments, populateGlobal } from 'vitest/environments' 
 console.log(builtinEnvironments) // { jsdom, happy-dom, node, edge-runtime }- populateGlobal实用函数用于将属性从对象移动到全局命名空间。
测试上下文
Vitest 的测试上下文允许你定义可在测试中使用的工具(utils)、状态(states)和固定装置(fixtures)。
- 每个测试回调的第一个参数是测试上下文。1 
 2
 3
 4
 5
 6import { it } from 'vitest' 
 it('should work', (ctx) => {
 // prints name of the test
 console.log(ctx.meta.name)
 })
- 内置的测试上下文。
- context.meta
 包含关于测试的元数据的只读对象。
- context.expect
 绑定到当前测试的 expect API。
- 每个测试的上下文都不同。可以在 beforeEach和afterEachhooks 中访问和扩展它们。1 
 2
 3
 4
 5
 6
 7
 8
 9
 10import { beforeEach, it } from 'vitest' 
 beforeEach(async (context) => {
 // extend context
 context.foo = 'bar'
 })
 it('should work', ({ foo }) => {
 console.log(foo) // 'bar'
 })
- 可以通过添加聚合(aggregate)类型 TestContext, 为你的自定义上下文属性提供类型支持。如果只想为特定的1 
 2
 3
 4
 5declare module 'vitest' { 
 export interface TestContext {
 foo?: string
 }
 }beforeEach、afterEach、it或testhooks 提供属性类型,则可以将类型作为泛型传递。1 
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13interface LocalTestContext { 
 foo: string
 }
 beforeEach<LocalTestContext>(async (context) => {
 // typeof context is 'TestContext & LocalTestContext'
 context.foo = 'bar'
 })
 it<LocalTestContext>('should work', ({ foo }) => {
 // typeof foo is 'string'
 console.log(foo) // 'bar'
 })
扩展断言(Matchers)
由于 Vitest 兼容 Chai 和 Jest,所以可以根据个人喜好使用 chai.use API 或者 expect.extend。
- 可以使用对象包裹断言的形式调用 expect.extend方法扩展默认的断言。断言方法可以访问上下文1 
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11expect.extend({ 
 // 第一个参数是接收值,其余参数将直接传给断言
 toBeFoo(received, expected) {
 const { isNot } = this
 return {
 // 请勿根据 isNot 参数更改你的 "pass" 值,Vitest 为你做了这件事情
 pass: received === 'foo',
 message: () => `${received} is${isNot ? ' not' : ''} foo`,
 }
 },
 })this对象中的这些属性:
- isNot
 如果断言是在- not方法上调用的(- expect(received).not.toBeFoo()),则返回- true。
- promise
 如果断言是在- resolved/rejected中调用的,它的值将包含此断言的名称。否则,它将是一个空字符串。
- equals
 这是一个工具函数,他可以帮助你比较两个值。如果是相同的则返回- true,反之返回- false。这个方法几乎在每个断言内部都有使用。默认情况下,它支持非对称的断言。
- utils
 它包含了一系列工具函数,可以使用它们来显示信息。
this 上下文也包含了当前测试的信息,可以通过调用 expect.getState() 来获取它,有用的属性是:
- currentTestName
 当前测试的全称(包括 describe 块)。
- testPath
 当前测试的路径。
断言的返回值应该兼容如下接口:
| 1 | interface MatcherResult { | 
- 使用 TypeScript 时,可以使用以下代码扩展默认的 Matchers 接口:1 
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11interface CustomMatchers<R = unknown> { 
 toBeFoo(): R
 }
 declare global {
 namespace Vi {
 interface Assertion extends CustomMatchers {}
 interface AsymmetricMatchersContaining extends CustomMatchers {}
 }
 // jest.Matchers interface will also work.
 }
源码内联测试
Vitest 还提供了一种方式,可以运行与你的代码实现放在一起的测试,允许测试与实现共享相同的闭包,并且能够在不导出的情况下针对私有状态进行测试。可用于:
- 小范围的功能或工具的单元测试
- 原型设计
- 内联断言
 对于更复杂的测试,比如组件测试或 E2E 测试,建议使用单独的测试文件取而代之。
- 首先,在 if (import.meta.vitest)代码块内写一些测试代码并放在文件的末尾,例如:1 
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16// src/index.ts 
 // 函数实现
 export function add(...args: number[]) {
 return args.reduce((a, b) => a + b, 0)
 }
 // 源码内的测试套件
 if (import.meta.vitest) {
 const { it, expect } = import.meta.vitest
 it('add', () => {
 expect(add()).toBe(0)
 expect(add(1)).toBe(1)
 expect(add(1, 2, 3)).toBe(6)
 })
 }
- 更新 Vitest 配置文件内的 includeSource以获取到src/下的文件:1 
 2
 3
 4
 5
 6
 7
 8// vite.config.ts 
 import { defineConfig } from 'vitest/config'
 export default defineConfig({
 test: {
 includeSource: ['src/**/*.{js,ts}'],
 },
 })
- 执行测试。1 npx vitest 
- 对于生产环境的构建,你需要设置配置文件内的 define选项,让打包器清除无用的代码。1 
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11// vite.config.ts 
 import { defineConfig } from 'vitest/config'
 export default defineConfig({
 + define: {
 + 'import.meta.vitest': 'undefined',
 + },
 test: {
 includeSource: ['src/**/*.{js,ts}']
 },
 })
- 要获得对 import.meta.vitest的 TypeScript 支持,添加vitest/importMeta到tsconfig.json:1 
 2
 3
 4
 5
 6
 7
 8// tsconfig.json 
 {
 "compilerOptions": {
 "types": [
 + "vitest/importMeta"
 ]
 }
 }
快照
当希望确保函数的输出不会意外更改时,可以使用快照测试,兼容 Jest 快照测试。使用快照时,Vitest 将获取给定值的快照,将其比较时将参考存储在测试旁边的快照文件。如果两个快照不匹配,则测试将失败:要么更改是意外的,要么参考快照需要更新到测试结果的新版本。
要将一个值快照,你可以使用 expect() 的 toMatchSnapshot() API:
| 1 | import { expect, it } from 'vitest' | 
此测试在第一次运行时,Vitest 会创建一个快照文件,如下所示:
| 1 | // Vitest Snapshot v1 | 
快照文件应该与代码更改一起提交,并作为代码审查过程的一部分进行审查。在随后的测试运行中,Vitest 会将执行的输出与之前的快照进行比较。如果他们匹配,测试就会通过。如果它们不匹配,要么测试运行时在你的代码中发现了应该修复的错误,要么实现已经更改,需要更新快照:
- 在监听(watch)模式下, 你可以在终端中键入 u键直接更新失败的快照。
- 或者,你可以在 CLI 中使用 --update或-u标记,vitest -u使 Vitest 进入快照更新模式。
指定超时阈值
你可以选择将超时阈值(以毫秒为单位)作为第三个参数传递给测试。默认值为 5 秒。
| 1 | import { test } from 'vitest' | 
Hooks 也可以接收超时阈值,默认值为 5 秒。
| 1 | import { beforeAll } from 'vitest' | 
选择、跳过、待办测试套件和测试
- 使用 .only仅运行某些测试套件或测试。1 
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20import { assert, describe, it } from 'vitest' 
 // 仅运行此测试套件(以及标记为 Only 的其他测试套件)
 describe.only('suite', () => {
 it('test', () => {
 assert.equal(Math.sqrt(4), 3)
 })
 })
 describe('another suite', () => {
 it('skipped test', () => {
 // 已跳过测试,因为测试在 Only 模式下运行
 assert.equal(Math.sqrt(4), 3)
 })
 it.only('test', () => {
 // 仅运行此测试(以及标记为 Only 的其他测试)
 assert.equal(Math.sqrt(4), 2)
 })
 })
- 使用 .skip以避免运行某些测试套件或测试。1 
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15import { assert, describe, it } from 'vitest' 
 describe.skip('skipped suite', () => {
 it('test', () => {
 // 已跳过此测试套件,无错误
 assert.equal(Math.sqrt(4), 3)
 })
 })
 describe('suite', () => {
 it.skip('skipped test', () => {
 // 已跳过此测试,无错误
 assert.equal(Math.sqrt(4), 3)
 })
 })
- 使用 .todo留存将要实施的测试套件和测试的待办事项。1 
 2
 3
 4
 5
 6
 7
 8
 9import { describe, it } from 'vitest' 
 // 此测试套件的报告中将显示一个条目
 describe.todo('unimplemented suite')
 // 此测试的报告中将显示一个条目
 describe('suite', () => {
 it.todo('unimplemented test')
 })
类型测试
- 从 Vitest 0.25.0 开始,Vitest 附带 expect-type 包,可以使用 expectTypeOf或assertType语法为你的类型编写测试。
- 在测试文件中触发的任何类型错误都将被视为测试错误,因此可以使用任何类型技巧来测试项目中的类型。
- 默认情况下,*.test-d.ts文件中的所有测试都被视为类型测试,但可以使用typecheck.include配置选项更改它。
- Vitest 不运行或编译这些文件,它们仅由编译器静态分析,因此你不能使用任何动态语句,所以不能使用动态测试名称和 test.each、test.runIf、test.skipIf、test.each、test.concurrentAPI,但可以使用其他 API,例如test、describe、.only、.skip和.todo。
- 使用 CLI 标志,如 --allowOnly和-t也支持类型检查。1 
 2
 3
 4
 5
 6
 7
 8
 9
 10import { assertType, expectTypeOf } from 'vitest' 
 import { mount } from './mount.js'
 test('my types work properly', () => {
 expectTypeOf(mount).toBeFunction()
 expectTypeOf(mount).parameter(0).toMatchTypeOf<{ name: string }>()
 // @ts-expect-error name is a string
 assertType(mount({ name: 42 }))
 })
- 在 package.json文件scripts部分添加如下命令:Vitest 使用1 
 2
 3
 4
 5{ 
 "scripts": {
 "typecheck": "vitest typecheck"
 }
 }tsc --noEmit或vue-tsc --noEmit,具体取决于配置。
同时运行多个测试
在连续测试中使用 .concurrent 将会并发运行它们。
| 1 | import { describe, it } from 'vitest' | 
在测试套件中使用 .concurrent,则其中的每个测试用例都将并发运行。
| 1 | import { describe, it } from 'vitest' | 
还可以将 .skip、.only 和 .todo 用于并发测试套件和测试用例。
Vitest 配置
Vitest 的主要优势之一是它与 Vite 的统一配置,vitest 将读取你的根目录 vite.config.ts 以匹配插件,例如 resolve.alias 和 plugins 的配置将会在 Vitest 中开箱即用。
- 创建 vitest.config.ts,优先级将会最高。
- 将 --config选项传递给 CLI,例如vitest --config ./path/to/vitest.config.ts。
- 在 defineConfig上使用process.env.VITEST或mode属性(如果没有被覆盖,将设置为test)有条件地在vite.config.ts中应用不同的配置。
使用 vite 的 defineConfig 还需要将 三斜线指令 写在配置文件的顶部,可以参考下面的格式:
| 1 | /// <reference types="vitest" /> | 
使用 vitest 的 defineConfig 可以参考下面的格式:
| 1 | import { defineConfig } from 'vitest/config' | 
如果有需要,可以获取到 Vitest 的默认选项以扩展它们:
| 1 | import { configDefaults, defineConfig } from 'vitest/config' | 
对象模拟(Mocking)
在编写测试时,可能会因为时间问题,需要创建内部或外部服务的 “假” 版本,这通常被称为 对象模拟 操作。Vitest 通过 vi 提供了一些实用的函数用于解决这个问题。你可以使用 import { vi } from 'vitest' 或者 全局配置 进行访问它(当启用 全局配置 时)。
| 1 | import { expect, vi } from 'vitest' | 
Vitest 支持 happy-dom 或 jsdom 来模拟 DOM 和浏览器 API。Vitest 并不内置它们,所以需要安装:
| 1 | npm i -D happy-dom | 
然后,更改 environment 配置文件中的选项:
| 1 | // vite.config.ts | 
测试覆盖率
- Vitest 通过 c8 支持本机代码覆盖率。同时也支持 istanbul。默认情况下,启用 c8。可以通过将 test.coverage.provider设置为c8或istanbul来选择覆盖工具:1 
 2
 3
 4
 5
 6
 7
 8
 9
 10// vite.config.ts 
 import { defineConfig } from 'vitest/config'
 export default defineConfig({
 test: {
 coverage: {
 provider: 'istanbul', // or 'c8'
 },
 },
 })
- 当你启动 Vitest 进程时,它会提示你自动安装相应的支持包:1 
 2
 3npm i -D @vitest/coverage-c8 
 # or
 npm i -D @vitest/coverage-istanbul
- 要在启用的情况下进行测试,在 CLI 中传递 --coverage标志:1 
 2
 3
 4
 5
 6{ 
 "scripts": {
 "test": "vitest",
 "coverage": "vitest run --coverage"
 }
 }
- 要对其进行配置,需要在配置文件中设置 test.coverage选项:1 
 2
 3
 4
 5
 6
 7
 8
 9// vite.config.ts 
 import { defineConfig } from 'vitest/config'
 export default defineConfig({
 test: {
 coverage: {
 reporter: ['text', 'json', 'html'],
 },
 },
 })
- 也可以通过将 'custom'传递给test.coverage.provider来配置你的自定义覆盖率提供者:自定义覆盖率提供者需要一个1 
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11// vite.config.ts 
 import { defineConfig } from 'vitest/config'
 export default defineConfig({
 test: {
 coverage: {
 provider: 'custom',
 customProviderModule: 'my-custom-coverage-provider',
 },
 },
 })customProviderModule选项,它是一个模块名称或从中加载CoverageProviderModule的路径。它必须将实现CoverageProviderModule的对象导出为默认导出:1 
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28// my-custom-coverage-provider.ts 
 import type {
 CoverageProvider,
 CoverageProviderModule,
 ResolvedCoverageOptions,
 Vitest,
 } from 'vitest'
 const CustomCoverageProviderModule: CoverageProviderModule = {
 getProvider(): CoverageProvider {
 return new CustomCoverageProvider()
 },
 // Implements rest of the CoverageProviderModule ...
 }
 class CustomCoverageProvider implements CoverageProvider {
 name = 'custom-coverage-provider'
 options!: ResolvedCoverageOptions
 initialize(ctx: Vitest) {
 this.options = ctx.config.coverage
 }
 // Implements rest of the CoverageProvider ...
 }
 export default CustomCoverageProviderModule
- 运行覆盖率报告时,会在项目的根目录中创建一个 coverage文件夹。如果想将它移动到不同的目录,使用vite.config.js文件中的test.coverage.reportsDirectory属性:1 
 2
 3
 4
 5
 6
 7
 8
 9import { defineConfig } from 'vite' 
 export default defineConfig({
 test: {
 coverage: {
 reportsDirectory: './tests/unit/coverage',
 },
 },
 })
命令行
在安装了 Vitest 的项目中,可以在 npm 脚本中使用 vitest 脚本,或者直接使用 npx vitest 运行它。以下是脚手架 Vitest 项目中的默认 npm 脚本:
| 1 | { | 
- vitest 在开发环境下默认启动时使用 监听模式(watch mode),当你修改源代码或测试文件时,Vitest 智能搜索模块依赖树并只重新运行相关测试,就像 HMR 在 Vite 的工作方式一样!
- 在 CI 环境(当 process.env.CI出现时)中以运行模式(run mode)启动,在不监视文更改的情况下执行单次运行。
- 可以使用 vitest watch或vitest run明确指定所需的模式。
- 可以使用 CLI 按名称筛选测试文件,例如 vitest basic将只执行包含basic路径名的测试文件。1 
 2basic.test.ts 
 basic-foo.test.ts
- Vitest 默认启动多线程,可以通过 CLI 中的 --no-threads禁用。
- Vitest 还隔离了每个测试文件的运行环境,因此一个文件中的运行环境改变不会影响其他文件,可以通过将 --no-isolate传递给 CLI 来禁用隔离(以正确性换取运行性能)。
- 还可以指定其他 CLI 选项,例如 --port或--https,在项目中运行npx vitest --help获取有关 CLI 选项的完整列表。
Vitest UI
Vitest 由 Vite 提供能力,在运行测试时有一个开发服务器。这允许 Vitest 提供一个漂亮的 UI 界面来查看并与测试交互。
- 安装:1 npm i -D @vitest/ui 
- 通过传入 --ui参数来启动测试的 UI 界面:1 vitest --ui 
- 通过 http://localhost:51204/__vitest__/可以访问 Vitest UI 界面。
- Vitest 0.26.0 开始, UI 也可以用作测试报告器。在 Vitest 配置中使用 'html'报告器生成 HTML 输出并预览测试结果,如果仍想在终端中实时查看测试的运行情况,不要忘记将default报告器添加到reporters选项。1 
 2
 3
 4
 5
 6
 7// vitest.config.ts 
 export default {
 test: {
 reporters: ['default', 'html'],
 },
 }
- 要预览你的 HTML 报告,可以使用 vite preview命令:可以使用1 npx vite preview --base __vitest__ --outDir ./html --outputFile=<path>配置选项配置输出,./html/index.html是默认值。
API
- describe 描述, 会形成一个作用域,用来组织测试和基准,使报告更加清晰 
- test 别名 it,定义了一组关于测试期望的方法,接收测试名称和一个含有测试期望的函数,可以提供一个超时时限(以毫秒为单位)用于指定等待多长时间后终止测试,默认为 5 秒,也可以通过 - testTimeout选项进行全局配置
- expect 用来创建断言 - not 将会否定断言
- toBe 可用于断言基础对象是否相等
- toBeDefined 断言检查值是否不等于 undefined
- toBeUndefined 断言检查值是否等于 undefined
- toBeNull 简单地断言检查值是否为 null,是 .toBe(null) 的别名
- toBeNaN 简单地断言是否为 NaN,是 .toBe(NaN) 的别名
- toBeTruthy 会将检查值转换为布尔值,断言该值是否为 true
- toBeFalsy 会将检测值转换为布尔值,断言该值是否为 false
- toBeTypeOf 断言检查值是否属于接收的类型
- toBeInstanceOf 断言检查值是否为接收的类的实例
- toBeGreaterThan 断言检查值是否大于接收值
- toBeGreaterThanOrEqual 断言检查值是否大于等于接收值
- toBeLessThan 断言检查值是否小于接收值
- toBeLessThanOrEqual 断言检查值是否小于等于接收值
- toEqual 断言检查值是否等于接收值,或者是同样的结构,如果是对象类型(将会使用递归的方法进行比较)
- toStrictEqual 断言检查值是否等于接收值或者同样的结构,如果是对象类型(将会使用递归的方法进行比较),并且会比较它们是否是相同的类型
- toContain 断言检查值是否在数组中,还可以检查一个字符串是否为另一个字符串的子串
- toContainEqual 断言在数组中是否包含具有特定结构和值的元素,就像对每个元素进行 toEqual 操作
- toHaveLength 断言一个对象是否具有 .length 属性,并且为数值
- toHaveProperty 用于断言对象上是否存在指定 key 的属性,同时该方法还提供了一个可选参数,用于进行深度对比,就像使用 toEqual 比较接收到的属性值
- toMatch 断言字符串是否匹配指定的正则表达式或字符串
- toMatchObject 用于断言对象是否匹配指定的对象属性的子集,还可以传递对象数组。如果我们只想检查两个数组的元素数量是否匹配,该方法就会很有用,它不同于 arrayContaining ,它允许接收数组中的额外元素
- toThrowError 断言函数在调用时是否抛出错误,可以提供一个可选参数来测试是否引发了指定的错误:- 正则表达式:错误信息通过正则表达式匹配
- 字符串:错误消息包含指定子串
 
- resolves 可以从待处理的 Promise 中去展开它的值,并使用通常的断言语句来断言它的值
- rejects 可以来展开 Promise 被拒绝的原因,并使用通常的断言语句来断言它的值
 
- expect.assertions 在测试通过或失败后,它将会验证在测试期间调用了多少次断言,常用于检查异步代码是否被调用了 
- expect.anything 这种非对称匹配器与相等检查一起使用时,将始终返回 true,如果你只是想确保该属性存在时很有用 
- expect.any 这种非对称匹配器与相等检查一起使用时,仅当 value 是指定构造函数的实例时才会返回 true,如果你有一个每次都生成的值,并且只想知道它以正确的类型存在是很有用 
- expect.arrayContaining 当与相等检查一起使用时,如果 value 是一个数组并包含指定的选项,则此非对称匹配器将返回 true,可以将 expect.not 与此匹配器一起使用来否定预期值 
- expect.objectContaining 当与相等检查一起使用时,如果 value 具有相似的结构,则此非对称匹配器将返回 true,可以将 expect.not 与此匹配器一起使用来否定预期值 
- expect.stringContaining 当与相等检查一起使用时,如果 value 是字符串并且包含指定的子字符串,则此非对称匹配器将返回 true,可以将 expect.not 与此匹配器一起使用来否定预期值 
- expect.stringMatching 当与相等检查一起使用时,如果 value 是字符串并且包含指定的子字符串或字符串匹配正则表达式,则此非对称匹配器将返回 true,可以将 expect.not 与此匹配器一起使用来否定预期值 
- not- 1 
 2
 3
 4
 5
 6- import { expect, test } from 'vitest' 
 const input = Math.sqrt(16)
 expect(input).not.to.equal(2) // chai API
 expect(input).not.toBe(2) // jest API
- toEqual和- toBe之间的区别- 1 
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19- import { expect, test } from 'vitest' 
 const stockBill = {
 type: 'apples',
 count: 13,
 }
 const stockMary = {
 type: 'apples',
 count: 13,
 }
 test('stocks have the same properties', () => {
 expect(stockBill).toEqual(stockMary)
 })
 test('stocks are not the same', () => {
 expect(stockBill).not.toBe(stockMary)
 })
- toEqual和- toStrictEqual之间的区别- 1 
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12- import { expect, test } from 'vitest' 
 class Stock {
 constructor(type) {
 this.type = type
 }
 }
 test('structurally the same, but semantically different', () => {
 expect(new Stock('apples')).toEqual({ type: 'apples' })
 expect(new Stock('apples')).not.toStrictEqual({ type: 'apples' })
 })
- toHaveProperty- 1 
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41- import { expect, test } from 'vitest' 
 const invoice = {
 'isActive': true,
 'P.O': '12345',
 'customer': {
 first_name: 'John',
 last_name: 'Doe',
 location: 'China',
 },
 'total_amount': 5000,
 'items': [
 {
 type: 'apples',
 quantity: 10,
 },
 {
 type: 'oranges',
 quantity: 5,
 },
 ],
 }
 test('John Doe Invoice', () => {
 expect(invoice).toHaveProperty('isActive') // 断言 key 存在
 expect(invoice).toHaveProperty('total_amount', 5000) // 断言 key 存在且值相等
 expect(invoice).not.toHaveProperty('account') // 断言 key 不存在
 // 使用 dot 进行深度引用
 expect(invoice).toHaveProperty('customer.first_name')
 expect(invoice).toHaveProperty('customer.last_name', 'Doe')
 expect(invoice).not.toHaveProperty('customer.location', 'India')
 // 使用包含 key 的数组进行深度引用
 expect(invoice).toHaveProperty('items[0].type', 'apples')
 expect(invoice).toHaveProperty('items.0.type', 'apples') // 使用 dot 也可以工作
 // 在数组中包装你的 key 来避免它作为深度引用
 expect(invoice).toHaveProperty(['P.O'], '12345')
 })
- toMatch- 1 
 2
 3
 4
 5
 6- import { expect, test } from 'vitest' 
 test('top fruits', () => {
 expect('top fruits include apple, orange and grape').toMatch(/apple/)
 expect('applefruits').toMatch('fruit') // toMatch 也可以是一个字符串
 })
- toMatchObject- 1 
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41- import { expect, test } from 'vitest' 
 const johnInvoice = {
 isActive: true,
 customer: {
 first_name: 'John',
 last_name: 'Doe',
 location: 'China',
 },
 total_amount: 5000,
 items: [
 {
 type: 'apples',
 quantity: 10,
 },
 {
 type: 'oranges',
 quantity: 5,
 },
 ],
 }
 const johnDetails = {
 customer: {
 first_name: 'John',
 last_name: 'Doe',
 location: 'China',
 },
 }
 test('invoice has john personal details', () => {
 expect(johnInvoice).toMatchObject(johnDetails)
 })
 test('the number of elements must match exactly', () => {
 // 断言对象数组是否匹配
 expect([{ foo: 'bar' }, { baz: 1 }]).toMatchObject([
 { foo: 'bar' },
 { baz: 1 },
 ])
 })
- toThrowError- 1 
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22- import { expect, test } from 'vitest' 
 function getFruitStock(type) {
 if (type === 'pineapples') {
 throw new DiabetesError(
 'Pineapples is not good for people with diabetes'
 )
 }
 // 可以做一些其他的事情
 }
 test('throws on pineapples', () => {
 // 测试错误消息是否在某处显示 "diabetes" :这些是等效的
 expect(() => getFruitStock('pineapples')).toThrowError(/diabetes/)
 expect(() => getFruitStock('pineapples')).toThrowError('diabetes')
 // 测试确切的错误信息
 expect(() => getFruitStock('pineapples')).toThrowError(
 /^Pineapples is not good for people with diabetes$/
 )
 })
- resolves- 1 
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11- import { expect, test } from 'vitest' 
 async function buyApples() {
 return fetch('/buy/apples').then(r => r.json())
 }
 test('buyApples returns new stock id', async () => {
 // toEqual 现在返回一个 Promise ,所以我们必须等待它
 await expect(buyApples()).resolves.toEqual({ id: 1 }) // jest API
 await expect(buyApples()).resolves.to.equal({ id: 1 }) // chai API
 })
- rejects- 1 
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11- import { expect, test } from 'vitest' 
 async function buyApples(id) {
 if (!id)
 throw new Error('no id')
 }
 test('buyApples throws an error when no id provided', async () => {
 // toThrow 现在返回一个 Promise ,所以你必须等待它
 await expect(buyApples()).rejects.toThrow('no id')
 })
- expect.assertions- 1 
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17- import { expect, test } from 'vitest' 
 async function doAsync(...cbs) {
 await Promise.all(cbs.map((cb, index) => cb({ index })))
 }
 test('all assertions are called', async () => {
 expect.assertions(2)
 function callback1(data) {
 expect(data).toBeTruthy()
 }
 function callback2(data) {
 expect(data).toBeTruthy()
 }
 await doAsync(callback1, callback2)
 })
- expect.anything- 1 
 2
 3
 4
 5- import { expect, test } from 'vitest' 
 test('object has "apples" key', () => {
 expect({ apples: 22 }).toEqual({ apples: expect.anything() })
 })
- expect.any- 1 
 2
 3
 4
 5
 6- import { expect, test } from 'vitest' 
 import { generateId } from './generators'
 test('"id" is a number', () => {
 expect({ id: generateId() }).toEqual({ id: expect.any(Number) })
 })
- expect.arrayContaining- 1 
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12- import { expect, test } from 'vitest' 
 test('basket includes fuji', () => {
 const basket = {
 varieties: ['Empire', 'Fuji', 'Gala'],
 count: 3,
 }
 expect(basket).toEqual({
 count: 3,
 varieties: expect.arrayContaining(['Fuji']),
 })
 })
- expect.objectContaining- 1 
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15- import { expect, test } from 'vitest' 
 test('basket has empire apples', () => {
 const basket = {
 varieties: [
 {
 name: 'Empire',
 count: 1,
 },
 ],
 }
 expect(basket).toEqual({
 varieties: [expect.objectContaining({ name: 'Empire' })],
 })
 })
- expect.stringContaining- 1 
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12- import { expect, test } from 'vitest' 
 test('variety has "Emp" in its name', () => {
 const variety = {
 name: 'Empire',
 count: 1,
 }
 expect(basket).toEqual({
 name: expect.stringContaining('Emp'),
 count: 1,
 })
 })
- expect.stringMatching- 1 
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12- import { expect, test } from 'vitest' 
 test('variety ends with "re"', () => {
 const variety = {
 name: 'Empire',
 count: 1,
 }
 expect(basket).toEqual({
 name: expect.stringMatching(/re$/),
 count: 1,
 })
 })
使用
- 在 package.json文件scripts部分添加如下命令:1 
 2
 3
 4
 5
 6{ 
 "scripts": {
 "test": "vitest",
 "test:run": "vitest run"
 }
 }
- 配置 vite.config.ts:1 
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12/// <reference types="vitest" /> 
 // Configure Vitest (https://vitest.dev/config/)
 import { defineConfig } from 'vite'
 export default defineConfig({
 test: {
 /* for example, use global to avoid globals imports (describe, test, expect): */
 // globals: true,
 },
 })
- 定义文件。
- suite.test.ts- 1 
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15- import { assert, describe, expect, it } from 'vitest' 
 describe('suite name', () => {
 it('foo', () => {
 assert.equal(Math.sqrt(4), 2)
 })
 it('bar', () => {
 expect(1 + 1).eq(2)
 })
 it('snapshot', () => {
 expect({ foo: 'bar' }).toMatchSnapshot()
 })
 })
- sum.ts- 1 
 2
 3- export default function sum(...numbers:number[]){ 
 return numbers.reduce((total,number)=>total+number,0)
 }
- sum.test.ts- 1 
 2
 3
 4
 5
 6
 7- import sum from './sum' 
 import {describe,expect,it} from "vitest"
 describe("#sum", () => {
 it("returns 0 with no numbers", () => {
 expect(sum()).toBe(0)
 })
 })
- 运行测试 pnpm run test:run,在suite.test.ts文件目录下面生成了一个快照文件__snapshots__/suite.test.ts.snap:1 
 2
 3
 4
 5
 6
 7// Vitest Snapshot v1 
 exports[`suite name > snapshot 1`] = `
 {
 "foo": "bar",
 }
 `;
组件测试示例
- 定义组件:
- Case.tsx1 
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30import { defineComponent, ref, watchEffect } from 'vue' 
 export default defineComponent({
 name: 'TestComponent',
 props: {
 value: String,
 },
 emits: ['update:value'],
 setup(props, { emit }) {
 const local = ref('')
 watchEffect(() => {
 emit('update:value', local)
 })
 watchEffect(() => {
 local.value = props.value!
 })
 return {
 local,
 }
 },
 render() {
 return (
 <a-select v-model={[this.local, 'value']}>
 <a-select-option value="aaa">aaa</a-select-option>
 </a-select>
 )
 },
 })
- Link.tsx1 
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46import { defineComponent, PropType, ref } from "vue"; 
 export type IType = 'default' | 'primary' | 'success' | 'warning' | 'danger'| 'info'
 export type IColor = 'black' | 'blue' | 'green' | 'yellow'| 'red' | 'gray'
 export const props = {
 type: {
 type: String as PropType<IType>,
 default: "default",
 },
 color: {
 type: String as PropType<IColor>,
 default: "black",
 },
 plain: {
 type: Boolean,
 default: true,
 },
 href: {
 type: String,
 required: true,
 },
 } as const;
 export default defineComponent({
 name: "CLink",
 props,
 setup(props, { slots }) {
 return () => (
 <a
 class={`
 text-${props.plain ? props.color + "-500" : "white"}
 hover:text-${props.color}-400
 cursor-pointer
 text-lg
 hover:text-white
 transition duration-300 ease-in-out transform hover:scale-105
 mx-1
 decoration-none
 `}
 href={props.href}
 >
 {slots.default ? slots.default() : 'Link'}
 </a>
 );
 },
 });
- vitest 本身是不支持单元组件测试的,需要安装 Vue Test Utils:pnpm add @vue/test-utils jsdom -D。
- 配置:
- package.json1 
 2
 3
 4
 5
 6{ 
 "scripts": {
 "test": "vitest",
 "test:run": "vitest run"
 },
 }
- vite.config.ts1 
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14import { defineConfig } from 'vite' 
 import Vue from '@vitejs/plugin-vue'
 import Jsx from '@vitejs/plugin-vue-jsx'
 export default defineConfig({
 plugins: [Vue(), Jsx()],
 test: {
 globals: true,
 environment: 'jsdom',
 transformMode: {
 web: [/.[tj]sx$/],
 },
 },
 })
- 定义测试。
- case.test.ts1 
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17//创建一个包含被挂载和渲染的组件的Wrapper,和mount不同的是shallowMount仅限测试组件,不牵扯子组件内容 
 import { shallowMount } from '@vue/test-utils'
 import { expect, test } from 'vitest'
 import Case from '../src/Case'
 test('mount component', () => {
 const wrapper = shallowMount(Case, {
 props: {
 value: 'test',
 },
 global: {
 stubs: ['a-select', 'a-select-option'],
 },
 })
 //返回 Wrapper DOM 节点的 HTML 字符串到快照
 expect(wrapper.html()).toMatchSnapshot()
 })
- link.test.ts1 
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24import Link from '../src/link/Link' 
 import { shallowMount } from '@vue/test-utils'
 import { describe, expect, test } from 'vitest'
 //使用shallowMount()方法挂载组件,并使用expect断言方法来检验组件的渲染是否正确
 describe('Link', () => {
 test("mount @vue/test-utils", () => {
 const wrapper = shallowMount(Link, {
 slots: {
 default: 'Link'
 }
 });
 //断言
 expect(wrapper.text()).toBe("Link")
 })
 })
 //对组件颜色进行测试,测试默认link颜色
 describe("Link", () => {
 test("default color is black", () => {
 // 使用 shallowMount 方法挂载组件
 const wrapper = shallowMount(Link);
 // 断言组件默认颜色是否是 black
 expect(wrapper.props().color).toBe("black");
 });
 });
- 运行测试 pnpm run test:run,在case.test.ts文件目录下面生成了一个快照文件__snapshots__/case.test.ts.snap:1 
 2
 3// Vitest Snapshot v1 
 exports[`mount component 1`] = `"<a-select-stub value=\\"test\\"></a-select-stub>"`;