使用 vitest 做单元测试

本文最后更新于:2023年6月20日 凌晨

简介

vitest 是一个新的单元测试工具,它很快,默认支持 esm,兼容 jest api,可以被视为更好的 jest。在默认情况下,它支持以下正在使用的功能

  • 很快
  • 支持 esm
  • 支持 ts
  • 兼容 jest api
  • 支持 vite 的功能
  • 支持多框架 react/vue

官网: https://vitest.dev/

安装

vitest 内部依赖 vite,但不需要安装 vite。

1
npm i -D vitest

配置

好吧,实际上 vitest 是真正的零配置,支持 ts/esm/tsx,但如果你想要更多的功能,确实可以创建 vitest.config.ts 文件或者直接在 vite.config.ts 中添加配置。

基本示例

基本上,它与 jest 类似,在要测试文件的同级目录创建一个 __tests__ 文件夹,然后在其中创建一个文件,文件名以 .test.ts 结尾,文件内容如下

1
2
3
4
5
import { expect, it } from 'vitest'

it('hello', () => {
expect(1 + 2).eq(3)
})

然后运行 pnpm vitest hello.test.ts 就可以以监视模式运行单元测试了

如果有多个测试时可以使用 describe 分组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import { describe, expect, it } from 'vitest'

describe('math', () => {
it('add', () => {
expect(1 + 2).eq(3)
})
it('less', () => {
expect(1 - 2).eq(-1)
})
})

describe('string', () => {
it('hello', () => {
const hello = (name: string) => `hello ${name}`
expect(hello('world')).eq('hello world')
})
})

也可以使用 -t 参数指定要执行的测试,例如

1
pnpm vitest hello.test.ts -t 'add'

也可以使用 it.only/descrive.only 来指定要执行的测试

另外一些 beforeEach/afterEach 等钩子函数也是支持的

例如默认创建一个测试目录

1
2
3
4
5
6
7
8
9
10
const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
const tempPath = path.resolve(__dirname, '.temp', path.basename(__filename))
beforeEach(async () => {
await rm(tempPath, { recursive: true, force: true })
await mkdir(tempPath, { recursive: true })
})
afterEach(async () => {
await rm(tempPath, { recursive: true, force: true })
})

断言

vitest 默认内嵌 chaijs 作为断言库,并且兼容了 jest 的断言 api。

最有趣的是,它支持某些断言很简洁,例如断言一些常见的值

1
2
3
4
expect(true).true
expect(false).false
expect(undefined).undefined
expect(null).null

或者对比值

1
2
expect(1).eq(1)
expect({ name: 'world' }).deep.eq({ name: 'world' }) // 深度对比

断言数组中包含指定值

1
expect([1, 2, 3]).members([1, 2, 3])

web api polyfill

通常在 web 项目中测试时也会包含一些 dom api,例如 localStorage/indexedDB 等。

happy-dom

dom 相关的 api mock 可以通过 happy-dom 来实现,它是一个简单的 dom 实现,可以在 node 中运行,虽然 api 比 jsdom 少一些,但速度要更快。

配置

1
2
3
4
5
{
"test": {
"environment": "happy-dom"
}
}

使用 localStorage

1
2
3
4
expect(localStorage.getItem('test')).null
localStorage.setItem('test', 'test')
expect(localStorage.getItem('test')).eq('test')
localStorage.removeItem('test')

fetch

nodejs@18 支持了 fetch 请求,不再需要 polyfill。

indexedDB

遗憾的是 hyppy-dom 没有实现 indexedDB,所以我们需要使用 fake-indexeddb 做 polyfill。
这个不需要做什么配置,只需要在测试文件中引入即可

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
import 'fake-indexeddb/auto'
import { openDB } from 'idb'

it('indexeddb', async () => {
const db = await openDB<{
books: {
title: string
author: string
isbn: string
}
}>('test', 1, {
upgrade(db) {
db.createObjectStore('books', {
autoIncrement: true,
keyPath: 'isbn',
})
},
})
expect(await db.getAll('books')).empty
await db.add('books', {
title: 'Quarry Memories',
author: 'Fred',
isbn: 123456,
})
await db.add('books', {
title: 'Water Buffaloes',
author: 'Fred',
isbn: 234567,
})
expect(await db.getAll('books')).length(2)
})

局限性

  • vitest 有一个 vscode 插件,但目前尚未支持 monorepo,所以建议不要使用
  • vitest 基于 vite,__dirname/__filename 等变量在测试中可用,但实际上在 nodejs esm 中不可用
  • vitest 与 node:test 的 api 有重叠,尤其是 it/describe,要留意不要错误引用

使用 vitest 做单元测试
https://blog.rxliuli.com/p/a9f8e0634b3f476687a2e844470fba44/
作者
rxliuli
发布于
2023年2月22日
许可协议