下载工作台
前端框架精通

前端测试:Vitest 与 Testing Library

试读上半部分 · 解锁后可读全文

第 14 章 · 前端测试:Vitest 与 Testing Library

本章目标:区分 单元测试 / 组件测试 / E2E;配置 Vitest 并分别使用 Vue Test UtilsReact Testing Library;为 CourseCard、登录表单、API 编写可维护测试;理解覆盖率与 CI 门禁;了解 TDD 简要流程。

学时建议:3~4 小时

前置:本模块 ch01~ch05、ch12~ch13(表单与 axios)。


14.1 测试金字塔与选型

                    ┌─────────┐
                    │   E2E   │  Playwright / Cypress — 少量关键路径
                   ┌┴─────────┴┐
                   │ 组件测试   │  Vitest + Testing Library — 本章重点
                  ┌┴───────────┴┐
                  │  单元测试    │  纯函数、utils、zod schema
                 └───────────────┘
类型测什么速度维护成本贤紫后台示例
单元无副作用函数极快formatPriceproductSchema
组件UI 交互与渲染CourseCard、登录表单
集成多模块协作中高列表页 + MSW
E2E真实浏览器全流程登录→下单(ch16 答辩演示)
行业案例 · 贤紫优选商城:2025 Q2 起商户后台 PR 合并门槛为 单元+组件覆盖率 ≥ 60%(核心 api/stores/ ≥ 75%),回归测试时间从人工 2 小时降至 CI 6 分钟。

原则:多写组件与单元,E2E 只覆盖「登录、下单、退款」等金路径。


14.2 Vitest 配置(Vue / React 通用)

npm install -D vitest @vitest/coverage-v8 jsdom
# Vue
npm install -D @vue/test-utils @vitejs/plugin-vue
# React
npm install -D @testing-library/react @testing-library/user-event @testing-library/jest-dom
// vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue' // 或 react

export default defineConfig({
  plugins: [vue()],
  test: {
    globals: true,
    environment: 'jsdom',
    setupFiles: ['./src/test/setup.ts'],
    include: ['src/**/*.{test,spec}.{ts,tsx}'],
    coverage: {
      provider: 'v8',
      reporter: ['text', 'html', 'lcov'],
      include: ['src/**/*.{ts,tsx,vue}'],
      exclude: ['src/main.ts', 'src/mocks/**'],
      thresholds: {
        lines: 60,
        functions: 60,
        branches: 50,
      },
    },
  },
})
// package.json scripts
{
  "scripts": {
    "test": "vitest",
    "test:run": "vitest run",
    "test:coverage": "vitest run --coverage"
  }
}
// src/test/setup.ts(React)
import '@testing-library/jest-dom/vitest'

// Vue 可在此 global.stubs、pinia 等
配置项说明
environment: 'jsdom'模拟 DOM,组件测试必需
globals: true免每文件 import { describe, it, expect }
setupFiles全局 mock、jest-dom 匹配器

14.3 单元测试:纯函数与 Zod schema

// src/utils/formatPrice.test.ts
import { describe, it, expect } from 'vitest'
import { formatPrice } from './formatPrice'

describe('formatPrice', () => {
  it('formats CNY with two decimals', () => {
    expect(formatPrice(1999)).toBe('¥19.99')
  })

  it('handles zero', () => {
    expect(formatPrice(0)).toBe('¥0.00')
  })
})
// src/schemas/product.test.ts
import { productStep1Schema } from './product'

describe('productStep1Schema', () => {
  it('rejects negative price', () => {
    const result = productStep1Schema.safeParse({
      name: '测试商品',
      price: -1,
      sku: 'SKU-001',
    })
    expect(result.success).toBe(false)
  })

  it('accepts valid payload', () => {
    const result = productStep1Schema.safeParse({
      name: '贤紫优选茶叶',
      price: 88,
      sku: 'XZ-TEA-001',
    })
    expect(result.success).toBe(true)
  })
})

单元测试特征:不 import 组件、不 mount DOM、毫秒级执行。


14.4 Vue:测试 CourseCard

<!-- src/components/CourseCard.vue -->
<script setup lang="ts">
defineProps<{
  title: string
  level: string
  hours: string
  onEnroll?: () => void
}>()
</script>

<template>
  <article class="course-card" data-testid="course-card">
    <h3>{{ title }}</h3>
    <span class="level">{{ level }}</span>
    <span class="hours">{{ hours }}</span>
    <button type="button" @click="onEnroll?.()">立即选课</button>
  </article>
</template>
// src/components/CourseCard.spec.ts
import { describe, it, expect, vi } from 'vitest'
import { mount } from '@vue/test-utils'
import CourseCard from './CourseCard.vue'

describe('CourseCard', () => {
  const baseProps = {
    title: 'Vue 3 入门',
    level: '入门',
    hours: '约 4 小时',
  }

  it('renders title and meta', () => {
    const wrapper = mount(CourseCard, { props: baseProps })
    expect(wrapper.get('[data-testid="course-card"]').text()).toContain('Vue 3 入门')
    expect(wrapper.find('.level').text()).toBe('入门')
  })

  it('calls onEnroll when button clicked', async () => {
    const onEnroll = vi.fn()
    const wrapper = mount(CourseCard, { props: { ...baseProps, onEnroll } })
    await wrapper.get('button').trigger('click')
    expect(onEnroll).toHaveBeenCalledOnce()
  })
})
Vue Test Utils API用途
mount挂载组件
get / find查询 DOM
trigger('click')模拟事件
vi.fn()Mock 回调

查询优先级getByRole > getByLabelText > data-testid(与 RTL 一致)。


14.5 React:测试 CourseCard

与 Vue 版断言一致:渲染标题/等级;userEvent.click 按钮后 onEnroll 被调用一次。优先 getByRole('heading')getByRole('button')

RTL 哲学:从用户可见行为断言,不测内部 state。


以下内容需解锁后阅读

试读已结束。解锁本章 ¥5.00,或开通年度会员畅读全部教程。
年度会员 ¥199.00/年; 小紫 AI 工作台有效会员 ¥99.00/年

正文仅在服务端鉴权后下发,未付费无法获取下半部分内容。