FORMA

测试策略

Vue 应用常用 Vitest + @vue/test-utils 做单元/组件测试,Playwright 或 Cypress 做 E2E。见 Vue 基础

Vue 应用的测试可分为单元测试、组件测试、端到端(E2E)测试和快照测试。合理的测试策略可以提升代码质量和交付信心,同时避免过度测试带来的维护成本。

一、单元测试

单元测试针对函数、组合式函数或单个组件进行隔离验证。

1. 测试环境搭建:Vitest + @vue/test-utils

Vitest 是基于 Vite 的单元测试框架,与 Vite 配置无缝集成,速度极快。配合 @vue/test-utils(Vue 官方测试工具库)可以渲染和交互组件。

bash
npm install -D vitest @vue/test-utils jsdom

配置 vitest.config.ts

ts
import { defineConfig } from "vitest/config";
import vue from "@vitejs/plugin-vue";

export default defineConfig({
  plugins: [vue()],
  test: {
    environment: "jsdom", // 模拟浏览器环境
    globals: true, // 全局使用 describe/it/expect
  },
});

2. 组件挂载方法:mount vs shallowMount

  • mount:完全渲染组件及其所有子组件。适合测试组件之间的交互,但可能因为子组件复杂导致测试缓慢或难以定位问题。
  • shallowMount:仅渲染当前组件,子组件以占位符(stub)形式出现。用于隔离当前组件的逻辑,避免子组件的副作用。

选择原则

  • 测试组件自身逻辑(事件处理、计算属性、生命周期)→ shallowMount
  • 测试组件与其子组件的集成(如插槽内容传递、子组件触发事件) → mount 或使用 shallowMount + 手动 stub
js
import { mount, shallowMount } from "@vue/test-utils";
import Parent from "./Parent.vue";

// 浅渲染,子组件会被替换为 <child-stub>
const wrapper = shallowMount(Parent);
// 完全渲染
const wrapperFull = mount(Parent);

3. 异步测试:flushPromisesnextTick

Vue 的响应式更新和 Promise 异步操作需要特殊处理。

  • nextTick:等待 Vue 完成 DOM 更新。
  • flushPromises:等待所有微任务(Promise、async 函数)完成。
js
import { nextTick } from "vue";
import { flushPromises } from "@vue/test-utils";

test("异步数据加载", async () => {
  const wrapper = mount(AsyncComponent);
  // 触发异步操作(比如调用 API)
  wrapper.find("button").trigger("click");
  await flushPromises(); // 等待所有 Promise resolve
  await nextTick(); // 等待 DOM 更新
  expect(wrapper.text()).toContain("Loaded");
});

Vitest 中也可以使用 vi.useFakeTimers() 模拟定时器。

4. Mock 策略:vi.mock

Vitest 提供 vi.mock 来模拟模块,可以拦截 API 请求、composable 函数或 Pinia store。

  • 模拟 API 请求(如 axios 或 fetch):
js
import { vi } from 'vitest';
import axios from 'axios';
vi.mock('axios');
const mockedAxios = axios as vi.Mocked<typeof axios>;
mockedAxios.get.mockResolvedValue({ data: { name: 'Test' } });
  • 模拟 composable 函数
js
vi.mock("@/composables/useUser", () => ({
  useUser: vi.fn(() => ({
    user: ref({ name: "Mocked" }),
    isLoggedIn: ref(true),
  })),
}));
  • 模拟 Pinia store:直接使用 createPinia + 模拟 store 的返回值,或者使用 vi.mock 模拟整个 store 模块。

5. 组合式函数的纯函数测试

组合式函数(composables)通常不依赖 DOM,可以直接在测试中调用并断言返回值。

js
import { useCounter } from "@/composables/useCounter";
import { describe, it, expect } from "vitest";

describe("useCounter", () => {
  it("should increment count", () => {
    const { count, increment } = useCounter(0);
    expect(count.value).toBe(0);
    increment();
    expect(count.value).toBe(1);
  });
});

需要对响应式行为进行测试时,可以创建 Vue 组件内部的 setup 环境,但通常直接调用足矣。

二、端到端测试(E2E)

E2E 测试模拟真实用户在浏览器中操作应用,验证整个系统的集成情况。

Cypress 与 Vue 集成

Cypress 是 Vue 社区常用的 E2E 工具,配置简单,并提供可视化运行器。

bash
npm install -D cypress @cypress/vue @cypress/vite-dev-server

配置 cypress.config.ts

ts
import { defineConfig } from "cypress";
export default defineConfig({
  component: {
    devServer: { framework: "vue", bundler: "vite" },
  },
  e2e: {
    baseUrl: "http://localhost:5173",
    setupNodeEvents(on, config) {},
  },
});

编写 E2E 测试(例如登录流程):

js
describe("Login", () => {
  it("should redirect after successful login", () => {
    cy.visit("/login");
    cy.get('[data-test="email"]').type("user@example.com");
    cy.get('[data-test="password"]').type("pass123");
    cy.get('[data-test="submit"]').click();
    cy.url().should("include", "/dashboard");
  });
});

Playwright 与 Vue 集成

Playwright 是 Cypress 的有力竞争者,支持多浏览器和更快的并行执行。

bash
npm install -D @playwright/test @playwright/experimental-ct-vue

配置和使用类似 Cypress,但 API 更为现代。

选择建议

  • 项目复杂度高、需要跨浏览器测试 → Playwright
  • 团队熟悉 JavaScript、希望快速上手 → Cypress

三、快照测试

快照测试将组件的渲染输出(HTML 结构)序列化为字符串并与存储的基准快照进行比对,用于捕获意外变更。

何时使用

  • 静态内容组件:如纯展示的 Header、Footer,内容几乎不变。
  • 样式回归测试:结合 CSS-in-JS 或 CSS Modules,验证样式是否被意外破坏。
  • API 响应数据快照:测试序列化数据格式。

何时避免

  • UI 组件频繁变动:每次需求迭代都需要更新快照,导致测试失效快,维护成本高。
  • 动态内容多:如包含时间戳、随机 ID 的组件,快照会频繁失败。
  • 跨平台差异:不同操作系统渲染的 HTML 可能有细微差别。

使用示例(Vitest + @vue/test-utils):

js
import { mount } from "@vue/test-utils";
import StaticBanner from "./StaticBanner.vue";

test("StaticBanner snapshot", () => {
  const wrapper = mount(StaticBanner);
  expect(wrapper.html()).toMatchSnapshot();
});

最佳实践:只对关键的代表性组件进行快照测试,而不是所有组件。快照文件应纳入版本控制并定期审查。

四、测试覆盖率策略

测试覆盖率是衡量测试完整性的指标,但追求 100% 覆盖率往往得不偿失。以下为推荐的优先级:

重点关注对象

  1. 组合式函数(composables):因为它们是可复用的业务逻辑,错误影响面大。
  2. 工具函数(utils):纯函数,容易测试,且通常是错误高发区域。
  3. 复杂计算属性(computed):包含条件判断或数据转换。
  4. 关键用户流程对应的组件:如支付表单、登录注册模块。

适当覆盖

  • UI 交互逻辑:按钮点击、表单提交、错误提示等,可使用 @vue/test-utils 进行交互测试。
  • 边界条件:如输入校验、空数据、加载状态。

无需过度追求

  • 简单的模板逻辑:如单纯的 v-for 渲染列表(除非包含复杂过滤)。
  • 第三方库的封装:假定库本身已测试。
  • 样式细节:样式应由视觉回归工具(如 Percy)或人工验收。

配置覆盖率工具(Vitest)

ts
// vitest.config.ts
export default defineConfig({
  test: {
    coverage: {
      provider: "v8", // 或 'istanbul'
      reporter: ["text", "html"],
      include: ["src/**/*.{vue,ts}"],
      exclude: ["src/main.ts", "src/**/*.d.ts"],
      thresholds: {
        statements: 80,
        branches: 70,
        functions: 80,
        lines: 80,
      },
    },
  },
});

指标解读

  • 行覆盖率:最容易达到,但不能保证逻辑正确。
  • 分支覆盖率:更能发现边缘条件遗漏。
  • 函数覆盖率:确保关键函数被调用。

注意:不要盲目追求覆盖率数字,应在 CI 中设置合理的阈值(如 70-80%),并允许团队根据模块重要性调整。

总结

测试类型工具/库核心关注点维护成本
单元测试Vitest + @vue/test-utils组合式函数、工具函数、组件逻辑
组件测试@vue/test-utils (mount/shallow)组件交互、props/emit 验证
E2E 测试Cypress / Playwright完整用户旅程、跨页面集成
快照测试Vitest (toMatchSnapshot)静态输出、防意外回归低 (但易产生噪音)
覆盖率Vitest / Jest 覆盖率报告量化测试充分性,指导补充测试自动

在实际项目中,推荐采用测试金字塔模型:大量单元测试(底层) + 适量组件测试(中间层) + 少量 E2E 测试(顶层)。将测试重点放在稳定且关键的业务逻辑上,可以减少维护负担,同时保证交付质量。

参考文献

以下链接在编写时均可正常访问:

资料说明
Vue:测试官方测试指南
Vitest单元测试框架
@vue/test-utils组件测试工具
PlaywrightE2E(英文)