CodeRoadMap
首页路线题库资源库文章社区
CodeRoadMap

程序员的学习成长路线图。内容免费开放,登录后可同步路线图与课时进度。

由 程序员柏风 独立维护 · 关于作者

学习

  • 路线图
  • 课程
  • 面试题
  • 文章
  • 我

更多

  • 资源
  • 社区
  • 关于作者
  • 内容说明

© 2026 CodeRoadMap

津ICP备2026012044号-1·津公网安备12011002026155号·coderoadmap@126.com
前端入门实战/课时/Vitest
RawBlame
课时目录12 节

前端入门实战

  1. 01HTML
  2. 02CSS
  3. 03JavaScript
  4. 04Git
  5. 05GitHub
  6. 06npm
  7. 07React
  8. 08Tailwind
  9. 09Vitest
  10. 10综合练习
  11. 11部署预览
  12. 12路线图对照

Vitest

组件测试。

25 分钟

Vitest:前端单元测试入门

这一课你将能做到

  1. 在 Vite/React 项目中安装和配置 Vitest
  2. 编写测试用例,用断言验证函数和组件的行为
  3. 测试异步代码和 React 组件,生成覆盖率报告

为什么要学

手动测试的问题是:改一行代码,要点开十几个页面验证。代码量大了以后,没人记得所有边界情况,也不敢重构——怕改坏不知道。单元测试就是自动化脚本,每次改完代码跑一遍,几秒内告诉你有没有破坏已有功能。

Vitest 是 Vite 生态的测试框架,由 Vite 团队开发,速度极快,API 和 Jest 完全兼容。现代前端项目标配:Vite + Vitest + React Testing Library。

先建立图像

测试的基本结构:

describe(测试套件,一组相关测试)
└── it / test(单个测试用例)
    ├── 准备数据(Arrange)
    ├── 执行操作(Act)
    └── 断言结果(Assert)

一个测试文件通常包含多个 describe 块,每个 describe 包含多个 it 用例。

跟着做

步骤 1:安装 Vitest

在 Vite + React 项目中:

npm install -D vitest @testing-library/react @testing-library/jest-dom jsdom

修改 package.json 添加脚本:

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

创建 vitest.config.js(或修改 vite.config.js):

import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";

export default defineConfig({
    plugins: [react()],
    test: {
        environment: "jsdom",
        globals: true,
        setupFiles: "./src/test/setup.js"
    }
});

创建 src/test/setup.js:

import "@testing-library/jest-dom";

步骤 2:第一个测试

创建 src/utils/math.js:

export function add(a, b) {
    return a + b;
}

export function divide(a, b) {
    if (b === 0) throw new Error("除数不能为 0");
    return a / b;
}

创建 src/utils/math.test.js:

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

describe("math 工具函数", () => {
    it("add 应该正确相加", () => {
        expect(add(1, 2)).toBe(3);
        expect(add(-1, 1)).toBe(0);
    });

    it("divide 应该正确相除", () => {
        expect(divide(10, 2)).toBe(5);
    });

    it("divide 除数为 0 时应该抛出错误", () => {
        expect(() => divide(10, 0)).toThrow("除数不能为 0");
    });
});

运行:

npm run test

Vitest 会进入 watch 模式,文件变化时自动重新运行测试。npm run test:run 只跑一次。

步骤 3:常用断言

import { expect } from "vitest";

// 相等
expect(2 + 2).toBe(4);                    // 严格相等(===)
expect({ a: 1 }).toEqual({ a: 1 });       // 深度相等(对象/数组)
expect([1, 2]).toContain(2);              // 数组包含
expect("hello world").toMatch(/world/);   // 正则匹配

// 真假
expect(true).toBeTruthy();
expect(false).toBeFalsy();
expect(null).toBeNull();
expect(undefined).toBeUndefined();
expect(1).toBeDefined();

// 数字
expect(10).toBeGreaterThan(5);
expect(10).toBeLessThan(20);
expect(0.1 + 0.2).toBeCloseTo(0.3);       // 浮点数精度

// 函数
const fn = vi.fn();
fn("hello");
expect(fn).toHaveBeenCalled();
expect(fn).toHaveBeenCalledWith("hello");

步骤 4:测试异步代码

// 被测函数
export async function fetchUser(id) {
    const res = await fetch(`https://api.example.com/users/${id}`);
    if (!res.ok) throw new Error("请求失败");
    return res.json();
}

// 测试
import { vi } from "vitest";

it("fetchUser 应该返回用户数据", async () => {
    // Mock fetch
    global.fetch = vi.fn(() =>
        Promise.resolve({
            ok: true,
            json: () => Promise.resolve({ id: 1, name: "张三" })
        })
    );

    const user = await fetchUser(1);
    expect(user.name).toBe("张三");
    expect(fetch).toHaveBeenCalledWith("https://api.example.com/users/1");
});

步骤 5:测试 React 组件

创建 src/components/Counter.jsx:

import { useState } from "react";

export default function Counter({ initial = 0 }) {
    const [count, setCount] = useState(initial);

    return (
        <div>
            <p data-testid="count">计数:{count}</p>
            <button onClick={() => setCount(count + 1)}>+1</button>
            <button onClick={() => setCount(count - 1)}>-1</button>
        </div>
    );
}

创建 src/components/Counter.test.jsx:

import { render, screen, fireEvent } from "@testing-library/react";
import Counter from "./Counter";

describe("Counter 组件", () => {
    it("应该显示初始计数", () => {
        render(<Counter initial={5} />);
        expect(screen.getByTestId("count")).toHaveTextContent("计数:5");
    });

    it("点击 +1 按钮应该增加计数", () => {
        render(<Counter />);
        const button = screen.getByText("+1");
        fireEvent.click(button);
        expect(screen.getByTestId("count")).toHaveTextContent("计数:1");
    });

    it("点击 -1 按钮应该减少计数", () => {
        render(<Counter />);
        fireEvent.click(screen.getByText("-1"));
        expect(screen.getByTestId("count")).toHaveTextContent("计数:-1");
    });
});

React Testing Library 的核心理念:像用户一样测试——找文字、点按钮、看结果,而不是测试内部实现细节。

步骤 6:生成覆盖率报告

npm install -D @vitest/coverage-v8
npm run coverage

终端会显示覆盖率表格,coverage/ 目录下有 HTML 报告。覆盖率指标:

  • Statements:语句覆盖率
  • Branches:分支覆盖率(if/else)
  • Functions:函数覆盖率
  • Lines:行覆盖率

建议目标:核心业务逻辑 80% 以上。

讲清楚要点

  1. 测试金字塔:单元测试(多、快、便宜)> 集成测试 > E2E 测试(少、慢、贵)。入门阶段先掌握单元测试。
  2. AAA 模式:Arrange(准备)、Act(执行)、Assert(断言)。每个测试用例都遵循这个结构,清晰可读。
  3. 测试行为,不是实现:不要测试「state 是否变成了 x」,而是测试「用户看到的文本是否变成了 x」。实现细节重构后测试不应失效。
  4. Mock 外部依赖:网络请求、定时器、本地存储等外部依赖都要 mock,保证测试快速、稳定、可重复。
  5. Vitest 和 Jest 的关系:API 几乎完全一样,Vitest 原生支持 ESM 和 TypeScript,在 Vite 项目中开箱即用,速度更快。

你可能会懵的地方

  • 测试文件放哪里? 两种主流方式:1) 和源文件同目录(math.js 和 math.test.js);2) 统一放 __tests__ 目录。推荐同目录,方便查找和维护。

  • toBe 和 toEqual 的区别? toBe 是 Object.is 严格相等,适合原始类型;toEqual 递归比较值,适合对象和数组。对象用 toBe 会失败,因为引用不同。

  • 为什么测试 React 组件要用 data-testid? 当文字内容会变化(比如多语言)或没有唯一文字时,用 data-testid 是稳定的选择器。但优先用 getByRole、getByText 等语义化查询。

  • watch 模式太吵了,只想跑一次? vitest run 只执行一次并退出,适合 CI 环境。开发时用默认的 watch 模式,保存文件自动重跑。

  • 测试通过了但代码有 bug? 测试只能验证你写了断言的场景。写测试时要思考边界情况:空数组、null、极大值、网络失败等。

错法对照

错法问题正解
测试里写 if 逻辑测试本身可能有 bug每个用例只测一个场景,用数据驱动
测试依赖执行顺序独立运行时失败每个测试独立,用 beforeEach 重置状态
直接测试私有方法重构后测试全挂通过公开接口测试,私有方法是实现细节
异步测试不加 await测试提前结束,假阳性所有 async 操作前加 await
Mock 所有东西测试失去意义只 mock 外部依赖(网络、时间、随机数)
一个 it 里塞 10 个断言失败时难以定位一个 it 聚焦一个行为,或拆分多个 it
忽略错误路径测试只测 happy path必须测试错误和边界情况

卡点急救

  1. 测试报「Cannot find module」:检查 Vitest 配置中的 alias 是否和 Vite 一致;检查文件扩展名(.jsx 需要正确配置)。
  2. React 组件测试报「document is not defined」:环境没配 jsdom,检查 vitest.config.js 的 test.environment。
  3. 异步测试超时:默认超时 5 秒,检查是否忘记 await;或显式增加超时 it("...", async () => {...}, 10000)。
  4. 覆盖率报告空白:确认 @vitest/coverage-v8 已安装;确认 package.json 的 coverage 脚本正确。

面试常问

  • 单元测试、集成测试、E2E 测试的区别? 单元测试测单个函数/组件;集成测试测多个模块协作;E2E 测试模拟真实用户操作完整流程。金字塔:单元测试最多,E2E 最少。

  • TDD 是什么? 测试驱动开发:先写失败的测试 → 写最少代码让测试通过 → 重构。红-绿-重构循环。能提升代码质量和设计,但学习曲线较陡。

  • Mock、Stub、Spy 的区别? Mock:完全模拟一个对象;Stub:替代某个方法返回固定值;Spy:监听函数调用,不改变行为。Vitest 中 vi.fn() 是 Mock/Spy,vi.spyOn() 是 Spy。

  • 为什么测试要快? 测试慢了就没人愿意跑。单元测试应该毫秒级,避免真实网络请求、文件 IO、数据库操作。慢测试移到集成/E2E 层。

自测三问

  1. 写一个函数 isEven(n) 和它的完整测试用例(包含正数、负数、零)。
  2. 测试 React 组件时,为什么推荐用 getByText 而不是直接查 DOM class?
  3. toBe 和 toEqual 分别适用于什么场景?

小结一句话

测试是代码的安全网:用 AAA 结构写用例、用断言验证行为、用 mock 隔离依赖——改代码不再心惊胆战,重构有了底气。

延伸

  • Vitest 官方文档
  • React Testing Library 文档
  • 下一课:综合练习——用 React + Tailwind 做一个完整项目。
加载本节练习…
← 上一课Tailwind下一课 →综合练习