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
开头,后跟环境名称的注释:
// @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
属性的对象:
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 环境:
import { builtinEnvironments, populateGlobal } from 'vitest/environments'
console.log(builtinEnvironments) // { jsdom, happy-dom, node, edge-runtime }
populateGlobal
实用函数用于将属性从对象移动到全局命名空间。
测试上下文
Vitest 的测试上下文允许你定义可在测试中使用的工具(utils)、状态(states)和固定装置(fixtures)。
- 每个测试回调的第一个参数是测试上下文。
import { it } from 'vitest'
it('should work', (ctx) => {
// prints name of the test
console.log(ctx.meta.name)
})
- 内置的测试上下文。
- context.meta
包含关于测试的元数据的只读对象。 - context.expect
绑定到当前测试的 expect API。
- 每个测试的上下文都不同。可以在
beforeEach
和afterEach
hooks 中访问和扩展它们。
import { beforeEach, it } from 'vitest'
beforeEach(async (context) => {
// extend context
context.foo = 'bar'
})
it('should work', ({ foo }) => {
console.log(foo) // 'bar'
})
- 可以通过添加聚合(aggregate)类型
TestContext
, 为你的自定义上下文属性提供类型支持。
declare module 'vitest' {
export interface TestContext {
foo?: string
}
}
如果只想为特定的 beforeEach
、afterEach
、it
或 test
hooks 提供属性类型,则可以将类型作为泛型传递。
interface 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
方法扩展默认的断言。
expect.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
当前测试的路径。
断言的返回值应该兼容如下接口:
interface MatcherResult {
pass: boolean
message: () => string
// 如果你传了这些参数,它们将自动出现在 diff 信息中,
// 所以即便断言不通过,你也不必自己输出 diff
actual?: unknown
expected?: unknown
}
- 使用 TypeScript 时,可以使用以下代码扩展默认的 Matchers 接口:
interface 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 (i
代码块内写一些测试代码并放在文件的末尾,例如:mport.meta.vitest)
// 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/
下的文件:
// vite.config.ts
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
includeSource: ['src/**/*.{js,ts}'],
},
})
- 执行测试。
npx vitest
- 对于生产环境的构建,你需要设置配置文件内的
define
选项,让打包器清除无用的代码。
// vite.config.ts
import { defineConfig } from 'vitest/config'
export default defineConfig({
+ define: {
+ 'import.meta.vitest ': 'undefined',
+ },
test: {
includeSource: ['src/**/*.{js,ts}']
},
})
- 要获得对
i
的 TypeScript 支持,添加mport.meta.vitest vitest/importMeta
到tsconfig.json
:
// tsconfig.json
{
"compilerOptions": {
"types": [
+ "vitest/importMeta"
]
}
}
快照
当希望确保函数的输出不会意外更改时,可以使用快照测试,兼容 Jest 快照测试。使用快照时,Vitest 将获取给定值的快照,将其比较时将参考存储在测试旁边的快照文件。如果两个快照不匹配,则测试将失败:要么更改是意外的,要么参考快照需要更新到测试结果的新版本。
要将一个值快照,你可以使用 expect()
的 toMatchSnapshot()
API:
import { expect, it } from 'vitest'
it('renders correctly', () => {
const result = toUpperCase('foobar')
expect(result).toMatchSnapshot()
})
此测试在第一次运行时,Vitest 会创建一个快照文件,如下所示:
// Vitest Snapshot v1
exports['toUpperCase 1'] = '"FOOBAR"'
快照文件应该与代码更改一起提交,并作为代码审查过程的一部分进行审查。在随后的测试运行中,Vitest 会将执行的输出与之前的快照进行比较。如果他们匹配,测试就会通过。如果它们不匹配,要么测试运行时在你的代码中发现了应该修复的错误,要么实现已经更改,需要更新快照:
- 在监听(watch)模式下, 你可以在终端中键入
u
键直接更新失败的快照。 - 或者,你可以在 CLI 中使用
--update
或-u
标记,vitest -u
使 Vitest 进入快照更新模式。
指定超时阈值
你可以选择将超时阈值(以毫秒为单位)作为第三个参数传递给测试。默认值为 5 秒。
import { test } from 'vitest'
test('name', async () => { /* ... */ }, 1000)
Hooks 也可以接收超时阈值,默认值为 5 秒。
import { beforeAll } from 'vitest'
beforeAll(async () => { /* ... */ }, 1000)
选择、跳过、待办测试套件和测试
- 使用
.only
仅运行某些测试套件或测试。
import { 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
以避免运行某些测试套件或测试。
import { 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
留存将要实施的测试套件和测试的待办事项。
import { 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.concurrent
API,但可以使用其他 API,例如test
、describe
、.only
、.skip
和.todo
。 - 使用 CLI 标志,如
--allowOnly
和-t
也支持类型检查。
import { 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
部分添加如下命令:
{
"scripts": {
"typecheck": "vitest typecheck"
}
}
Vitest 使用 tsc --noEmit
或 vue-tsc --noEmit
,具体取决于配置。
同时运行多个测试
在连续测试中使用 .concurrent
将会并发运行它们。
import { describe, it } from 'vitest'
// 标记为concurrent的两个测试将并行运行
describe('suite', () => {
it('serial test', async () => {
/* ... */
})
it.concurrent('concurrent test 1', async ({ expect }) => {
/* ... */
})
it.concurrent('concurrent test 2', async ({ expect }) => {
/* ... */
})
})
在测试套件中使用 .concurrent
,则其中的每个测试用例都将并发运行。
import { describe, it } from 'vitest'
// 此套件中的所有测试都将并行运行
describe.concurrent('suite', () => {
it('concurrent test 1', async ({ expect }) => {
/* ... */
})
it('concurrent test 2', async ({ expect }) => {
/* ... */
})
it.concurrent('concurrent test 3', async ({ expect }) => {
/* ... */
})
})
还可以将 .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
上使用p
或rocess.env.VITEST mode
属性(如果没有被覆盖,将设置为test
)有条件地在vite.config.ts
中应用不同的配置。
使用 vite
的 defineConfig
还需要将 三斜线指令
写在配置文件的顶部,可以参考下面的格式:
/// <reference types="vitest" />
import { defineConfig } from 'vite'
export default defineConfig({
test: {
/* 使用global避免全局导入(description、test、expect) */
// globals: true,
},
})
使用 vitest
的 defineConfig
可以参考下面的格式:
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
// ...
},
})
如果有需要,可以获取到 Vitest 的默认选项以扩展它们:
import { configDefaults, defineConfig } from 'vitest/config'
export default defineConfig({
test: {
exclude: [...configDefaults.exclude, 'packages/template/*'],
},
})
对象模拟(Mocking)
在编写测试时,可能会因为时间问题,需要创建内部或外部服务的 “假” 版本,这通常被称为 对象模拟 操作。Vitest 通过 vi 提供了一些实用的函数用于解决这个问题。你可以使用 import { vi } from 'vitest'
或者 全局配置 进行访问它(当启用 全局配置 时)。
import { expect, vi } from 'vitest'
const fn = vi.fn()
fn('hello', 1)
expect(vi.isMockFunction(fn)).toBe(true)
expect(fn.mock.calls[0]).toEqual(['hello', 1])
fn.mockImplementation(arg => arg)
fn('world', 2)
expect(fn.mock.results[1].value).toBe('world')
Vitest 支持 happy-dom 或 jsdom 来模拟 DOM 和浏览器 API。Vitest 并不内置它们,所以需要安装:
npm i -D happy-dom
# or
npm i -D jsdom
然后,更改 environment
配置文件中的选项:
// vite.config.ts
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
environment: 'happy-dom', // or 'jsdom', 'node'
},
})
测试覆盖率
- Vitest 通过 c8 支持本机代码覆盖率。同时也支持 istanbul。默认情况下,启用 c8。可以通过将
test.coverage.provider
设置为c8
或istanbul
来选择覆盖工具:
// vite.config.ts
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
coverage: {
provider: 'istanbul', // or 'c8'
},
},
})
- 当你启动 Vitest 进程时,它会提示你自动安装相应的支持包:
npm i -D @vitest/coverage-c8
# or
npm i -D @vitest/coverage-istanbul
- 要在启用的情况下进行测试,在 CLI 中传递
--coverage
标志:
{
"scripts": {
"test": "vitest",
"coverage": "vitest run --coverage"
}
}
- 要对其进行配置,需要在配置文件中设置
test.coverage
选项:
// vite.config.ts
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
coverage: {
reporter: ['text', 'json', 'html'],
},
},
})
- 也可以通过将
'custom'
传递给test.coverage.provider
来配置你的自定义覆盖率提供者:
// vite.config.ts
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
coverage: {
provider: 'custom',
customProviderModule: 'my-custom-coverage-provider',
},
},
})
自定义覆盖率提供者需要一个 customProviderModule
选项,它是一个模块名称或从中加载 CoverageProviderModule
的路径。它必须将实现 CoverageProviderModule
的对象导出为默认导出:
// 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
属性:
import { defineConfig } from 'vite'
export default defineConfig({
test: {
coverage: {
reportsDirectory: './tests/unit/coverage',
},
},
})
命令行
在安装了 Vitest 的项目中,可以在 npm 脚本中使用 vitest 脚本,或者直接使用 npx vitest
运行它。以下是脚手架 Vitest 项目中的默认 npm 脚本:
{
"scripts": {
"test": "vitest",
"coverage": "vitest run --coverage"
}
}
- vitest 在开发环境下默认启动时使用
监听模式(watch mode)
,当你修改源代码或测试文件时,Vitest 智能搜索模块依赖树并只重新运行相关测试,就像 HMR 在 Vite 的工作方式一样! - 在 CI 环境(当
p
出现时)中以rocess.env.CI 运行模式(run mode)
启动,在不监视文更改的情况下执行单次运行。 - 可以使用
vitest watch
或vitest run
明确指定所需的模式。 - 可以使用 CLI 按名称筛选测试文件,例如
vitest basic
将只执行包含basic
路径名的测试文件。
basic.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 界面来查看并与测试交互。
- 安装:
npm i -D @vitest/ui
- 通过传入
--ui
参数来启动测试的 UI 界面:
vitest --ui
- 通过
http://localhost:51204/__vitest__/
可以访问 Vitest UI 界面。 - Vitest 0.26.0 开始, UI 也可以用作测试报告器。在 Vitest 配置中使用
'html'
报告器生成 HTML 输出并预览测试结果,如果仍想在终端中实时查看测试的运行情况,不要忘记将default
报告器添加到reporters
选项。
// vitest.config.ts
export default {
test: {
reporters: ['default', 'html'],
},
}
- 要预览你的 HTML 报告,可以使用
vite preview
命令:
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
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
之间的区别
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
之间的区别
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
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
import { expect, test } from 'vitest'
test('top fruits', () => {
expect('top fruits include apple, orange and grape').toMatch(/apple/)
expect('applefruits').toMatch('fruit') // toMatch 也可以是一个字符串
})
toMatchObject
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
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
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
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
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
import { expect, test } from 'vitest'
test('object has "apples" key', () => {
expect({ apples: 22 }).toEqual({ apples: expect.anything() })
})
expect.any
import { expect, test } from 'vitest'
import { generateId } from './generators'
test('"id" is a number', () => {
expect({ id: generateId() }).toEqual({ id: expect.any(Number) })
})
expect.arrayContaining
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
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
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
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
部分添加如下命令:
{
"scripts": {
"test": "vitest",
"test:run": "vitest run"
}
}
- 配置
vite.config.ts
:
/// <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
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
export default function sum(...numbers:number[]){
return numbers.reduce((total,number)=>total+number,0)
}
sum.test.ts
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
:
// Vitest Snapshot v1
exports[`suite name > snapshot 1`] = `
{
"foo": "bar",
}
`;
组件测试示例
- 定义组件:
- Case.tsx
import { 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.tsx
import { 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.json
{
"scripts": {
"test": "vitest",
"test:run": "vitest run"
},
}
- vite.config.ts
import { 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.ts
//创建一个包含被挂载和渲染的组件的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.ts
import 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
:
// Vitest Snapshot v1
exports[`mount component 1`] = `"<a-select-stub value=\\"test\\"></a-select-stub>"`;