切换语言
切换主题

Vitest 单元测试实战:从配置到 TDD 开发流程

引言

Jest 配置一个 ESM 项目要折腾多久?ts-jest、babel-jest、jest.config.js… 还要处理各种模块解析问题。我曾经花了一整个下午,就为了让 Jest 能正确识别 .vue 文件的 import 语句。

而 Vitest 只需要一行配置。

这不是夸张。当我在一个 Vite 项目里第一次跑 vitest 时,看着测试用例几秒钟就跑完,那种感觉就像是换掉一台卡了三年的旧电脑——清爽。

Vitest 有多快?官方数据和社区实测都显示:冷启动约 200ms(Jest 要 2-4 秒),500 个测试用例大概 8 秒跑完(Jest 要 45 秒左右)。更香的是,它和 Vite 共享配置,原生支持 TypeScript,API 和 Jest 几乎一样——迁移成本可能就半小时。

这篇文章会带你从零配置到完整 TDD 流程,包括 Mocking 技巧和 Coverage 设置。不管你是想在新项目里用 Vitest,还是想把老项目从 Jest 迁移过来,都能找到需要的东西。

Vitest 是什么?为什么它这么快?

简单说,Vitest 是 Vite 原生的测试框架。

如果你已经在用 Vite 构建项目,那 Vitest 基本就是”开箱即用”。它直接复用 Vite 的配置——别名、环境变量、CSS 处理,全都自动继承。不需要额外配置 Jest 那一堆 transformmoduleFileExtensionsmoduleNameMapper

它的核心优势有三个:

速度快。 冷启动大概 200 毫秒,Jest 通常要 2-4 秒。大型项目更明显:500 个测试用例,Vitest 约 8 秒,Jest 要 45 秒左右(数据来自 DEV Community 2026 年的实测)。这差距不是一点半点。

Jest 兼容。 API 几乎一样——describeitexpectvi.fn(),写法和 Jest 没区别。从 Jest 迁移,基本就是改改 import 路径的事儿。

智能监听。 它有个叫 “HMR for tests” 的功能:改了代码,只重跑相关的测试,不是全部重来。开发时那种即时反馈的感觉,很爽。

说完了”是什么”,接下来看怎么装。

安装与配置

先说系统要求:Vite 版本不低于 6.0.0,Node 版本不低于 20.0.0。如果你的项目比较新,这些应该都满足。

安装

一条命令:

npm install -D vitest

就这样。不需要装一堆 @types/jestts-jestjest-environment-jsdom… Vitest 原生支持 TypeScript。

配置文件

两种方式:要么在 vite.config.ts 里加 test 字段,要么单独建一个 vitest.config.ts

如果项目简单,直接用 vite.config.ts

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

export default defineConfig({
  test: {
    globals: true,  // 全局变量,不用每次 import { describe, it, expect }
    environment: 'node', // 或 'jsdom'(测试浏览器环境)
    include: ['tests/**/*.test.ts'],
    coverage: {
      provider: 'v8',
      reporter: ['text', 'html', 'lcov'],
    },
  },
})

globals: true 这行很实用——开了之后,describeitexpect 全部变成全局变量,不用每个测试文件都 import 一遍。就像 Jest 一样。

如果你的测试需要 DOM API(比如测组件渲染),把 environment 改成 'jsdom',然后安装 jsdom

npm install -D jsdom

运行脚本

package.json 里加两行:

{
  "scripts": {
    "test": "vitest",
    "test:run": "vitest run"
  }
}

npm test 进入监听模式(改代码自动重跑),npm run test:run 跑一次就退出(CI 环境用这个)。

配置就这么点。和 Jest 那一大堆 presettransformmoduleFileExtensions 比起来,省事太多。

编写单元测试

测试文件命名习惯用 .test.ts.spec.ts,放在 tests/ 目录或者跟源文件同级。看团队习惯。

基础结构

一个最简单的测试:

import { describe, it, expect } from 'vitest'
import { add, divide } from './math'

describe('Math utilities', () => {
  it('should add two numbers', () => {
    expect(add(2, 3)).toBe(5)
  })

  it('should throw on division by zero', () => {
    expect(() => divide(10, 0)).toThrow('Division by zero')
  })
})

describe 是测试分组,it 定义单个测试用例,expect 做断言。

如果你开了 globals: true,import 那行可以省掉。

常用断言

几个最常用的:

// 基本相等
expect(value).toBe(5)            // 严格相等(===)
expect(obj).toEqual({ a: 1 })    // 深度相等

// 真值判断
expect(value).toBeTruthy()
expect(value).toBeFalsy()
expect(value).toBeNull()

// 异常
expect(() => fn()).toThrow()
expect(() => fn()).toThrow('Error message')

// 数字比较
expect(n).toBeGreaterThan(10)
expect(n).toBeLessThanOrEqual(5)

// 数组/字符串包含
expect(arr).toContain('item')
expect(str).toMatch(/pattern/)

测试过滤

开发时只想跑某个测试?用 .only

it.only('只跑这个测试', () => { ... })

临时跳过某个测试?用 .skip

it.skip('暂时不跑', () => { ... })

这俩对 describe 也生效:describe.only(...)describe.skip(...)

跑起来看看效果。监听模式下,改代码后立刻能看到测试结果变绿或变红——这种即时反馈,比 Jest 种每次都要等几秒钟启动的体验,好太多了。

TDD 开发流程实战

TDD(测试驱动开发)的核心理念很简单:先写测试,再写代码。

听起来反直觉,但实际用起来会发现,它逼着你在写代码之前先想清楚”这个函数应该做什么”。而不是写完代码再补测试——那时候测试往往变成”凑覆盖率”的形式主义。

下面用一个实战案例演示完整流程。我们要实现一个邮箱验证函数 validateEmail

Step 1:写测试,还没实现代码

先新建测试文件:

// tests/validateEmail.test.ts
import { describe, it, expect } from 'vitest'
import { validateEmail } from '../src/validateEmail'

describe('validateEmail', () => {
  it('should return true for valid email', () => {
    expect(validateEmail('test@example.com')).toBe(true)
  })

  it('should return false for invalid email', () => {
    expect(validateEmail('invalid')).toBe(false)
  })
})

此时 validateEmail 函数还不存在,跑测试肯定报错。但没关系——这正是 TDD 的第一步:让测试失败。

Step 2:实现最小代码

现在创建函数,写最简单的实现让测试通过:

// src/validateEmail.ts
export function validateEmail(email: string): boolean {
  return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)
}

跑测试:npm test

看到两个测试都绿了?好,Step 2 完成。

Step 3:加更多边界测试

基本的通过了,但邮箱验证还有很多边界情况。加几个:

// tests/validateEmail.test.ts (追加)
it('should return false for empty string', () => {
  expect(validateEmail('')).toBe(false)
})

it('should return false for email without domain', () => {
  expect(validateEmail('user@')).toBe(false)
})

it('should return false for email with spaces', () => {
  expect(validateEmail('test @example.com')).toBe(false)
})

跑测试——如果都通过,说明正则写得好。如果有失败,就修正正则。

Step 4:重构

测试都通过了,现在可以放心重构代码。比如把正则改成更严谨的版本,或者加注释:

// src/validateEmail.ts
const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/

export function validateEmail(email: string): boolean {
  if (!email || email.trim() === '') {
    return false
  }
  return EMAIL_REGEX.test(email)
}

改完再跑测试——依然全绿。这就是 TDD 的好处:重构时有测试兜底,改错了立刻知道。

为什么 TDD 好用?

说实话,刚开始我也不习惯”先写测试”。但用了几次后发现:

  1. 想清楚再动手:写测试时就是在设计函数的行为,强制你把输入输出想明白。
  2. 快速迭代:Vitest 监听模式下,改代码秒级反馈,不用等。
  3. 重构安全:有测试覆盖,改代码心不慌。

从简单的函数开始试一试,比如工具函数、格式化函数。上手之后再扩展到复杂逻辑。

Mocking 高级技巧

测单元测试时,很多时候需要”假装”一些外部依赖——API 请求、数据库查询、第三方库。这时候就要用 Mock。

Vitest 提供了一套和 Jest 类似的 Mock API,核心是 vi 对象。

vi.fn():Mock 单个函数

最简单的用法,创建一个假的函数:

import { vi, describe, it, expect } from 'vitest'

describe('vi.fn() demo', () => {
  it('tracks calls', () => {
    const mockFn = vi.fn()

    mockFn('hello')
    mockFn('world')

    expect(mockFn).toHaveBeenCalledTimes(2)
    expect(mockFn).toHaveBeenNthCalledWith(1, 'hello')
  })
})

还可以预设返回值:

const mockFn = vi.fn().mockReturnValue('mocked result')
// 或者异步返回
const asyncMock = vi.fn().mockResolvedValue({ data: 'ok' })

vi.mock():Mock 整个模块

测一个函数,但它依赖外部 API?直接 mock 那个模块:

import { vi, describe, it, expect, beforeEach } from 'vitest'
import { fetchUser } from './api'
import { UserService } from './UserService'

// Mock api 模块
vi.mock('./api', () => ({
  fetchUser: vi.fn().mockResolvedValue({ id: 1, name: 'Alice' })
}))

describe('UserService', () => {
  beforeEach(() => {
    vi.clearAllMocks() // 每个测试前清掉之前的调用记录
  })

  it('should fetch user', async () => {
    const service = new UserService()
    const user = await service.getUser(1)

    expect(user.name).toBe('Alice')
    expect(fetchUser).toHaveBeenCalledWith(1)
  })
})

vi.mock() 会在模块导入前执行,所以放在文件顶部。

vi.spyOn():监控真实函数

有时候不想完全替换函数,只想”监听”它的调用:

import { vi, describe, it, expect, afterEach } from 'vitest'
import { calculator } from './calculator'

describe('spyOn demo', () => {
  afterEach(() => {
    vi.restoreAllMocks()
  })

  it('tracks add calls', () => {
    const addSpy = vi.spyOn(calculator, 'add')

    const result = calculator.add(2, 3)

    expect(result).toBe(5) // 原函数正常执行
    expect(addSpy).toHaveBeenCalledWith(2, 3) // 同时记录调用
  })
})

spyOnmock 更温和——函数照常工作,只是多了个”监控器”。

Mock 全局对象

测试时不想真的发 HTTP 请求?Mock 全局 fetch

vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
  ok: true,
  json: () => Promise.resolve({ data: 'mocked' })
}))

或者用 Vitest 的 vi.stubGlobal 来 mock windowlocalStorage 等浏览器全局对象。

Mock 这块是测试里最 tricky 的部分。建议先从简单的 vi.fn() 开始,熟练再上 vi.mock()。记住每次测试后清理 Mock 状态(clearAllMocksrestoreAllMocks),不然不同测试之间会互相污染。

Coverage 与最佳实践

测试覆盖率是衡量代码质量的一个参考指标。Vitest 支持两种 Coverage provider:v8(更快,原生支持)和 istanbul(兼容性更好)。一般用 v8 就够了。

配置 Coverage

vite.config.ts 里加:

test: {
  coverage: {
    provider: 'v8',
    reporter: ['text', 'html', 'lcov'], // 输出格式
    thresholds: {
      lines: 80,      // 行覆盖率阈值
      functions: 80,  // 函数覆盖率阈值
      branches: 70,   // 分支覆盖率阈值
    },
    exclude: ['node_modules/', 'tests/', '**/*.d.ts'],
  },
}

运行命令:

vitest run --coverage

终端会显示覆盖率报告,同时生成 coverage/ 目录,里面有 HTML 报告可以打开看哪些代码没覆盖到。

设置阈值的意义

thresholds 不是摆设——如果覆盖率达不到设定值,Vitest 会报错退出。这在 CI 环境很有用:强制代码达到一定测试质量,防止”凑合提交”。

当然,阈值别设太高。80% 是个合理起点,100% 反而容易让团队疲于应付。

CI/CD 集成

在 GitHub Actions 或其他 CI 里加一步:

# .github/workflows/test.yml
- name: Run tests with coverage
  run: npm run test:run -- --coverage

跑完后可以把 lcov 报告上传到 Codecov 或 Coveralls,可视化追踪覆盖率变化。

几个实用建议

  1. 别追求 100%:覆盖率数字好看不代表代码质量高。测关键逻辑,边缘场景可以放过。
  2. 先测核心路径:主流程的测试优先级最高,异常分支其次。
  3. 定期清理无用测试:测试也要维护,删掉那些过时的、冗余的。
  4. Watch 模式好习惯:开发时保持 vitest 监听运行,有问题立刻发现。

总结

Vitest 的核心优势说白了就是:快、省事、好用。

快——冷启动 200ms,跑完 500 个测试用例也就 8 秒,比 Jest 快了将近一个数量级。省事——和 Vite 共享配置,装完就能跑,不用折腾一堆 transform 和 moduleNameMapper。好用——API 和 Jest 基本一样,迁移成本很低。

如果你在用 Vite,直接选 Vitest。没有理由再折腾 Jest 的 ESM 配置问题。

如果你的项目还在用 Jest,不妨花半小时试着迁移一下。先装 Vitest,把测试文件的 import 改改,大部分情况能直接跑通。

至于 TDD——别想太复杂。从简单的工具函数开始,先写测试再写代码。习惯了之后你会发现,这种”先想清楚再动手”的方式,反而效率更高。

测试不是负担,是保障。花点时间把 Vitest 配好,后面写代码会更安心。

Vitest 单元测试配置与 TDD 流程

从零开始配置 Vitest 并实践 TDD 开发流程的完整步骤

⏱️ 预计耗时: 30 分钟

  1. 1

    步骤1: 安装 Vitest

    运行安装命令:

    ```bash
    npm install -D vitest
    ```

    系统要求:Vite >= 6.0.0,Node >= 20.0.0
  2. 2

    步骤2: 配置 vite.config.ts

    在配置文件中添加 test 字段:

    ```typescript
    import { defineConfig } from 'vitest/config'

    export default defineConfig({
    test: {
    globals: true,
    environment: 'node',
    include: ['tests/**/*.test.ts'],
    },
    })
    ```

    globals: true 可省去每次导入 describe、it、expect。
  3. 3

    步骤3: 添加运行脚本

    在 package.json 中添加:

    ```json
    {
    "scripts": {
    "test": "vitest",
    "test:run": "vitest run"
    }
    }
    ```

    npm test 为监听模式,npm run test:run 为单次运行(CI 用)。
  4. 4

    步骤4: 编写第一个测试

    创建测试文件 tests/math.test.ts:

    ```typescript
    import { describe, it, expect } from 'vitest'

    describe('Math', () => {
    it('should add numbers', () => {
    expect(1 + 1).toBe(2)
    })
    })
    ```

    运行 npm test 验证配置是否成功。
  5. 5

    步骤5: 实践 TDD 流程

    按照测试驱动开发流程:

    • Step 1:先写测试,定义函数预期行为
    • Step 2:实现最小代码让测试通过
    • Step 3:添加边界测试
    • Step 4:重构优化

    使用 Vitest 监听模式获得秒级反馈。
  6. 6

    步骤6: 配置 Coverage

    添加覆盖率配置:

    ```typescript
    coverage: {
    provider: 'v8',
    reporter: ['text', 'html'],
    thresholds: {
    lines: 80,
    functions: 80,
    },
    }
    ```

    运行 vitest run --coverage 生成报告。

常见问题

Vitest 和 Jest 有什么区别?
Vitest 是 Vite 原生的测试框架,与 Vite 共享配置,冷启动约 200ms(Jest 要 2-4 秒)。API 与 Jest 几乎完全兼容,迁移成本很低。主要优势是速度快、配置简单、原生支持 TypeScript。
如何从 Jest 迁移到 Vitest?
迁移步骤简单:

• 卸载 Jest 相关包,安装 vitest
• 把 jest.config.js 配置迁移到 vite.config.ts
• 将测试文件中的 import { describe, it, expect } from 'jest' 改为 from 'vitest'
• 把 jest.fn()、jest.mock() 改为 vi.fn()、vi.mock()

大部分情况下 30 分钟内可完成迁移。
Vitest 支持哪些测试环境?
Vitest 支持多种环境:node(默认,用于后端测试)、jsdom(浏览器 DOM 环境)、happy-dom(更快的 DOM 替代方案)。在配置中通过 environment 字段指定,测试浏览器组件需要安装 jsdom 包。
如何在 Vitest 中 Mock API 请求?
三种常用方式:

• vi.fn():Mock 单个函数,预设返回值
• vi.mock():Mock 整个模块,替换所有导出
• vi.spyOn():监控真实函数调用,不替换实现

记住在每个测试后用 vi.clearAllMocks() 或 vi.restoreAllMocks() 清理状态。
Vitest 的 Coverage 如何配置?
在 vite.config.ts 的 test.coverage 中配置 provider(推荐 v8)、reporter(text/html/lcov)和 thresholds(覆盖率阈值)。运行 vitest run --coverage 生成报告。达不到阈值会报错,适合 CI 环境强制代码质量。
TDD 开发流程的核心是什么?
TDD 核心是"红-绿-重构"循环:

• 红:先写失败的测试
• 绿:写最少代码让测试通过
• 重构:优化代码结构

配合 Vitest 监听模式,改代码后秒级反馈,能快速迭代。先写测试能帮你理清函数设计思路。

10 分钟阅读 · 发布于: 2026年4月14日 · 修改于: 2026年4月14日

评论

使用 GitHub 账号登录后即可评论