跳到主要内容

核心知识点

一、基础概念

1. Vitest 简介与定位

  • 说明: Vitest 是一个由 Vite 驱动的 极速 单元测试框架。它旨在利用 Vite 生态系统的优势(如快速的 HMR、基于 esbuild 的转译)来提供卓越的开发体验 (DX) 和性能。它的 API 设计与 Jest 高度兼容,使得从 Jest 迁移相对平滑。
  • 定位: 主要用于前端项目(尤其与 Vite 深度绑定的项目,如 Vue, React, Svelte 等)以及 Node.js 库的单元测试、集成测试。

2. 与 Jest、Mocha 等测试框架的对比

  • 说明:
    • Jest: 功能全面,生态成熟,配置相对独立。Vitest 兼容其大部分 API,但启动和执行速度通常更快,尤其是在 Vite 项目中,因为它共享 Vite 的配置和转换管道。Jest 需要额外的配置(如 Babel 或 ts-jest)来处理 TypeScript/JSX,而 Vitest 则开箱即用。
    • Mocha: 灵活、历史悠久的测试运行器,通常需要配合断言库 (Chai) 和 Mock 库 (Sinon) 使用。Vitest 则内置了这些能力,提供更一体化的体验。
  • 对比优势:
    • 速度: 利用 Vite 的按需编译和缓存,测试启动和重新运行速度快。
    • 配置: 可直接复用 vite.config.js 中的配置(如别名 alias、全局变量 define 等),减少重复配置。
    • HMR: 支持测试热更新,修改代码后只重新运行相关的测试。
    • 原生 ESM: 对 ES Modules 有更好的原生支持。
    • 内置能力: 开箱即用的 TypeScript/JSX 支持,内置 Chai 断言和 Sinon mock 功能(通过 vi 对象)。

3. 适用场景与优势

  • 适用场景:
    • 使用 Vite 构建工具的项目(最佳选择)。
    • 对测试执行速度有较高要求的项目。
    • 希望利用现代 JavaScript 特性(ESM, TS, JSX)进行测试的项目。
    • 寻求与 Jest 相似 API 以降低学习/迁移成本的团队。
  • 优势:
    • 极快的测试执行速度和 HMR。
    • 与 Vite 生态无缝集成,配置简洁。
    • 开箱即用的 TS/JSX 支持。
    • Jest 兼容的 API。
    • 支持多环境测试 (Node.js, jsdom, happy-dom, edge-runtime)。
    • 支持源码内测试 (Source-inlined testing)。
    • 提供 Vitest UI 进行可视化测试管理。

4. 安装与初始化

  • 说明: 通常作为开发依赖项安装。可以通过 npm, yarn, pnpm 等包管理器进行安装。

  • 示例代码:

    # 使用 npm 安装
    npm install -D vitest

    # 使用 yarn 安装
    yarn add -D vitest

    # 使用 pnpm 安装
    pnpm add -D vitest

    package.json 中添加脚本:

    {
    "scripts": {
    "test": "vitest", // 运行所有测试
    "test:run": "vitest run", // 运行一次,不监听
    "test:watch": "vitest watch", // 监听模式 (默认行为)
    "coverage": "vitest run --coverage" // 运行并生成覆盖率报告
    }
    }

二、配置与集成

1. 配置文件(vitest.config.ts/js)

  • 说明: Vitest 可以自动读取项目根目录下的 vitest.config.ts (或 .js, .mjs, .cjs) 文件。它也可以与 vite.config.ts 合并或独立存在。推荐使用 TypeScript 配置文件以获得类型提示。

  • 示例代码 (vitest.config.ts):

    import { defineConfig } from "vitest/config";
    import path from "path";

    export default defineConfig({
    test: {
    // Vitest 特有的配置项
    globals: true, // 是否启用全局 API (describe, it, expect 等)
    environment: "jsdom", // 测试环境:'node', 'jsdom', 'happy-dom', 'edge-runtime'
    setupFiles: ["./src/setupTests.ts"], // 测试环境设置文件
    coverage: {
    // 代码覆盖率配置
    provider: "v8", // 或 'istanbul'
    reporter: ["text", "json", "html"], // 报告格式
    },
    },
    resolve: {
    alias: {
    // 与 vite.config.js 中 alias 同步
    "@": path.resolve(__dirname, "./src"),
    },
    },
    });

2. 常用配置项

  • 说明:

    • include/exclude: 指定包含/排除哪些测试文件,使用 glob 模式。默认包含 **/*.{test,spec}.?(c|m)[jt]s?(x)
    • environment: 设置测试运行环境。node 适用于 Node.js 模块测试;jsdomhappy-dom 模拟浏览器环境,用于 DOM 相关测试;edge-runtime 模拟 Vercel Edge 函数等环境。
    • globals: 是否将 describe, it, expect, vi 等自动注入到全局作用域。推荐设置为 false 并显式导入 (import { describe, it, expect, vi } from 'vitest') 以获得更好的类型支持和避免全局污染。
    • alias: 配置路径别名,通常与 vite.config.ts 保持一致。
    • setupFiles: 指定一个或多个在测试环境建立后、测试文件执行前运行的脚本文件。常用于设置全局 Mocks、扩展 expect 断言、引入 polyfills 等。
    • coverage: 配置代码覆盖率报告,包括使用的引擎 (provider)、报告格式 (reporter)、包含/排除的文件 (include/exclude)、覆盖率阈值 (thresholds) 等。
  • 示例代码 (在 vitest.config.tstest 对象内):

    test: {
    include: ['src/**/*.{test,spec}.ts'],
    exclude: ['node_modules', 'dist'],
    environment: 'happy-dom', // 使用 happy-dom 模拟浏览器环境
    globals: false, // 不使用全局变量,需要手动导入
    setupFiles: './tests/setup.ts',
    alias: { // 这里是 test 作用域内的 alias,也可以配置在顶层 resolve.alias
    '~': path.resolve(__dirname, './src/components'),
    },
    coverage: {
    provider: 'istanbul',
    enabled: true, // 启用覆盖率
    reporter: ['text', 'lcov'],
    include: ['src/utils/**', 'src/components/**'],
    exclude: ['src/**/*.stories.ts'],
    thresholds: { // 设置覆盖率阈值
    lines: 80,
    functions: 70,
    branches: 70,
    statements: 80,
    },
    },
    }

3. 与 Vite 配合使用

  • 说明: Vitest 的最大优势之一是与 Vite 的无缝集成。它可以直接读取 vite.config.ts 中的大部分配置,如 resolve.alias, plugins, define 等。

  • 示例代码: 如果你已经有了 vite.config.tsvitest.config.ts 可以非常简洁,甚至可以合并:

    // vite.config.ts (同时包含 Vitest 配置)
    import { defineConfig } from "vite";
    import vue from "@vitejs/plugin-vue";
    import type { UserConfig as VitestUserConfigInterface } from "vitest/config"; // 引入 Vitest 配置类型
    import path from "path";

    const vitestConfig: VitestUserConfigInterface["test"] = {
    // 定义 Vitest 的 test 配置
    globals: true,
    environment: "jsdom",
    };

    export default defineConfig({
    plugins: [vue()],
    resolve: {
    alias: {
    "@": path.resolve(__dirname, "./src"),
    },
    },
    // 直接在 vite config 中添加 test 字段
    test: vitestConfig,
    });

4. 与 TypeScript 集成

  • 说明: Vitest 基于 Vite,而 Vite 原生支持 TypeScript。因此,Vitest 开箱即用地支持使用 TypeScript 编写测试文件 (.ts, .tsx),无需额外配置 ts-jest 或类似工具。确保你的 tsconfig.json 配置正确即可。

  • 示例代码 (math.test.ts):

    import { describe, it, expect } from "vitest";
    import { add } from "../src/math"; // 导入 TS 模块

    describe("Math functions", () => {
    it("should add two numbers correctly", () => {
    const result: number = add(1, 2); // 类型检查生效
    expect(result).toBe(3);
    // expect(result).toBe('3'); // TS 会在编译前提示类型错误 (如果启用了类型检查)
    });
    });

5. Babel/ESBuild 支持

  • 说明: Vitest 默认使用 ESBuild 进行代码转换,速度极快。如果你的项目依赖特定的 Babel 插件或预设,可以通过 Vite 的插件体系(如 @vitejs/plugin-babel)来集成 Babel。

  • 示例代码 (在 vite.config.tsvitest.config.ts 中配置 Babel 插件):

    // vite.config.ts 或 vitest.config.ts
    import { defineConfig } from "vite";
    import babel from "@vitejs/plugin-babel";

    export default defineConfig({
    plugins: [
    // 添加 Babel 插件,确保它在需要时运行
    babel({
    babelConfig: {
    // 这里放置你的 Babel 配置
    plugins: ["@babel/plugin-proposal-decorators"],
    },
    }),
    ],
    test: {
    // ... 其他 Vitest 配置
    },
    });

6. 环境变量与多环境测试

  • 说明: Vitest 继承了 Vite 的环境变量处理方式。可以通过 .env 文件(.env, .env.test, .env.local, .env.test.local)来加载环境变量。也可以在 vite.config.tsdefine 字段中定义全局常量。多环境测试主要通过 environment 配置项切换(如 'node' vs 'jsdom')。

  • 示例代码:

    • .env.test:

      VITE_API_URL=https://test.example.com
    • 在测试中使用:

      import { describe, it, expect } from "vitest";

      describe("Environment variables", () => {
      it("should access test environment variables", () => {
      // 通过 import.meta.env 访问 (Vite 方式)
      expect(import.meta.env.VITE_API_URL).toBe("https://test.example.com");
      // process.env 也可以访问,但推荐使用 import.meta.env
      // expect(process.env.VITE_API_URL).toBe('https://test.example.com');
      });
      });
    • vitest.config.ts 中使用 define:

      defineConfig({
      define: {
      __APP_VERSION__: JSON.stringify("1.0.0-test"),
      },
      test: {
      /* ... */
      },
      });

      测试中直接使用 __APP_VERSION__

7. CLI 命令与参数

  • 说明: Vitest 提供了丰富的命令行接口 (CLI) 来控制测试的执行。

  • 常用命令:

    • vitest: 启动 Vitest 并进入监听模式。
    • vitest run: 运行所有测试一次然后退出。
    • vitest watch: 明确进入监听模式(与直接运行 vitest 效果相同)。
    • vitest <pattern>: 只运行匹配模式的文件或测试名称。例如 vitest user 会运行文件名或 describe/it 描述中包含 "user" 的测试。
  • 常用参数:

    • --environment <env>: 覆盖配置文件中的 environment 选项。
    • --coverage: 启用代码覆盖率报告(即使配置中未启用)。
    • --ui: 启动 Vitest UI,一个可视化的测试界面。
    • --root <path>: 指定项目根目录。
    • --globals: 覆盖 globals 配置。
    • --watch: 覆盖 run 命令,强制进入监听模式。
    • -t <name>--testNamePattern <name>: 只运行名称匹配的测试用例 (it/test)。
    • -u--update: 更新快照 (Snapshot)。
  • 示例:

    # 只运行 user.test.ts 文件
    vitest user.test.ts

    # 运行所有测试并生成覆盖率报告
    vitest run --coverage

    # 启动 Vitest UI
    vitest --ui

    # 只运行包含 "login" 的测试用例
    vitest -t login

    # 更新快照
    vitest -u

三、测试用例编写

1. describe、it/test 基本结构

  • 说明: Vitest 沿用了 BDD (行为驱动开发) 风格的测试结构。

    • describe(name, fn): 用于将相关的测试用例分组,创建一个测试套件。可以嵌套。
    • it(name, fn)test(name, fn): 定义一个单独的测试用例。ittest 功能完全相同,可根据个人偏好选用。
  • 示例代码:

    import { describe, it, expect } from "vitest";

    // 定义一个测试套件
    describe("String Utils", () => {
    // 定义另一个嵌套的测试套件
    describe("capitalize", () => {
    // 定义一个测试用例
    it("should capitalize the first letter of a string", () => {
    const capitalize = (str: string) =>
    str.charAt(0).toUpperCase() + str.slice(1);
    expect(capitalize("hello")).toBe("Hello");
    });

    // 另一个测试用例
    it("should return empty string if input is empty", () => {
    const capitalize = (str: string) =>
    str ? str.charAt(0).toUpperCase() + str.slice(1) : "";
    expect(capitalize("")).toBe("");
    });
    });

    // 同级别的另一个测试用例
    test("toUpperCase function", () => {
    // 使用 test 关键字
    const toUpperCase = (str: string) => str.toUpperCase();
    expect(toUpperCase("world")).toBe("WORLD");
    });
    });

2. 断言(expect API)

  • 说明: Vitest 内置了 Chai 断言库,并通过 expect API 提供。它还扩展了一些 Jest 风格的匹配器 (Matchers)。断言用于验证代码的行为是否符合预期。

  • 常用匹配器:

    • .toBe(value): 严格相等 (===),用于比较原始类型或对象引用。
    • .toEqual(value): 深度相等,用于比较对象或数组的内容。
    • .toBeTruthy() / .toBeFalsy(): 判断值是否为真值/假值。
    • .toBeNull() / .toBeUndefined() / .toBeDefined(): 判断是否为 null/undefined/非 undefined
    • .toBeInstanceOf(Class): 判断实例是否属于某个类。
    • .toContain(item) / .toContainEqual(item): 判断数组或字符串是否包含某个元素(前者严格相等,后者深度相等)。
    • .toHaveLength(number): 判断数组或字符串的长度。
    • .toMatch(regexp | string): 判断字符串是否匹配正则表达式或包含子串。
    • .toThrow(error?): 判断函数是否抛出错误,可指定错误类型或消息。
    • .toMatchSnapshot() / .toMatchInlineSnapshot(): 快照测试 (详见下文)。
    • .resolves: 用于断言 Promise 成功解决后的值。
    • .rejects: 用于断言 Promise 被拒绝的原因。
    • .toHaveBeenCalled() / .toHaveBeenCalledTimes(number) / .toHaveBeenCalledWith(...args): 用于 Mock 函数的断言 (详见 Mocking 部分)。
  • 示例代码:

    import { describe, it, expect } from "vitest";

    describe("Assertions", () => {
    const user = { name: "Alice", age: 30 };
    const getUserPromise = () => Promise.resolve(user);
    const throwErrorFunc = () => {
    throw new Error("Something went wrong");
    };

    it("basic assertions", () => {
    expect(1 + 1).toBe(2); // 严格相等
    expect(user).toEqual({ name: "Alice", age: 30 }); // 深度相等
    expect(user.name).toBeTruthy(); // 真值
    expect(null).toBeNull(); // null 判断
    expect(user).toBeInstanceOf(Object); // 实例判断
    });

    it("array and string assertions", () => {
    const arr = [1, 2, 3];
    expect(arr).toContain(2); // 包含元素
    expect(arr).toHaveLength(3); // 长度判断
    expect("hello world").toMatch(/world/); // 正则匹配
    });

    it("exception assertion", () => {
    expect(throwErrorFunc).toThrow(); // 抛出任何错误
    expect(throwErrorFunc).toThrow("Something went wrong"); // 抛出特定消息的错误
    expect(throwErrorFunc).toThrow(Error); // 抛出特定类型的错误
    });

    it("promise assertions", async () => {
    await expect(getUserPromise()).resolves.toEqual(user); // Promise 成功
    const rejectPromise = () => Promise.reject(new Error("Failed"));
    await expect(rejectPromise()).rejects.toThrow("Failed"); // Promise 失败
    });
    });

3. 钩子函数(beforeAll、beforeEach、afterAll、afterEach)

  • 说明: 钩子函数允许在测试套件或每个测试用例执行前后运行设置 (setup) 和清理 (teardown) 代码。

    • beforeAll(fn, timeout?): 在当前 describe 块内的所有测试开始前运行一次。
    • beforeEach(fn, timeout?): 在当前 describe 块内的每个测试开始前运行一次。
    • afterAll(fn, timeout?): 在当前 describe 块内的所有测试结束后运行一次。
    • afterEach(fn, timeout?): 在当前 describe 块内的每个测试结束后运行一次。
    • 钩子函数的作用域是它们所在的 describe 块。顶层的钩子函数作用于整个文件。
  • 示例代码:

    import {
    describe,
    it,
    expect,
    beforeAll,
    beforeEach,
    afterAll,
    afterEach,
    } from "vitest";

    describe("Hooks Example", () => {
    let dbConnection: { data: string[] };
    let counter = 0;

    // 在所有测试开始前执行一次:连接数据库
    beforeAll(() => {
    console.log("beforeAll: Connecting to database...");
    dbConnection = { data: [] };
    });

    // 在每个测试开始前执行一次:重置计数器,向数据库添加测试数据
    beforeEach(() => {
    console.log("beforeEach: Resetting counter and adding test data");
    counter = 0;
    dbConnection.data.push(`test-${Date.now()}`);
    });

    // 在每个测试结束后执行一次:清理操作,例如重置 Mocks
    afterEach(() => {
    console.log("afterEach: Cleaning up after test");
    // vi.clearAllMocks(); // 如果使用了 mock
    });

    // 在所有测试结束后执行一次:断开数据库连接
    afterAll(() => {
    console.log("afterAll: Disconnecting from database...");
    dbConnection = null!; // 模拟断开连接
    });

    it("test 1: should increment counter", () => {
    console.log("Running test 1");
    counter++;
    expect(counter).toBe(1);
    expect(dbConnection.data.length).toBeGreaterThan(0); // 确认 beforeEach 生效
    });

    it("test 2: counter should be reset by beforeEach", () => {
    console.log("Running test 2");
    expect(counter).toBe(0); // beforeEach 会重置 counter
    counter += 5;
    expect(counter).toBe(5);
    expect(dbConnection.data.length).toBeGreaterThan(1); // 又添加了一条数据
    });
    });

4. 跳过与只运行(skip、only、todo)

  • 说明: Vitest 提供了控制测试执行流程的修饰符。

    • .skip: 附加到 describeit/test 上,临时跳过该测试套件或测试用例。
    • .only: 附加到 describeit/test 上,只运行标记了 .only 的测试套件或用例(在当前测试运行中)。如果有多个 .only,它们都会运行。
    • .todo: 附加到 it/test 上,标记一个待完成的测试用例。它会在测试报告中列出,但不会执行。
  • 示例代码:

    import { describe, it, test } from "vitest";

    describe("Execution Control", () => {
    // 这个测试套件会被跳过
    describe.skip("Skipped Suite", () => {
    it("this test will not run", () => {
    expect(true).toBe(false); // 因为被跳过,所以不会失败
    });
    });

    // 只运行这个测试套件中的测试 (如果其他地方没有 .only)
    describe.only("Focused Suite", () => {
    it("this test will run", () => {
    expect(1).toBe(1);
    });

    // 这个测试用例会被跳过
    it.skip("this specific test is skipped", () => {
    expect(1).toBe(2);
    });
    });

    describe("Normal Suite", () => {
    // 这个测试用例因为上面的 .only 而不会运行
    it("this test will likely not run unless specified otherwise", () => {
    expect(true).toBe(true);
    });

    // 标记一个待办测试
    test.todo("implement the feature and write test", () => {
    // 不需要实现,只是一个占位符
    });
    });

    // 如果存在 .only, 这个测试也不会运行
    it("another test that might not run", () => {
    expect(0).toBe(0);
    });

    // 如果有多个 .only, 这个也会运行
    // it.only('another focused test', () => {
    // expect('focus').toBe('focus');
    // });
    });

5. 并发测试(concurrent)

  • 说明: 使用 .concurrent 修饰符可以让同一个文件内的多个测试用例并行执行(在同一个 worker 线程内,利用事件循环)。这对于 I/O 密集型或耗时较长的异步测试可以提升速度。也可以用在 describe 上,使其内部所有测试用例默认并发执行。

  • 注意: 并发测试可能引入资源竞争或状态污染问题,需要谨慎使用,确保测试之间是独立的。

  • 示例代码:

    import { describe, it, expect } from "vitest";

    const delay = (ms: number) =>
    new Promise((resolve) => setTimeout(resolve, ms));

    // describe.concurrent('Concurrent Suite', () => { // 套件内所有测试并发
    describe("Concurrent Tests", () => {
    // 这三个测试会尝试并行执行
    it.concurrent("test A should pass after 100ms", async () => {
    await delay(100);
    expect(true).toBe(true);
    });

    it.concurrent("test B should pass after 50ms", async () => {
    await delay(50);
    expect(1 + 1).toBe(2);
    });

    it.concurrent("test C should pass after 150ms", async () => {
    await delay(150);
    expect("hello").toBe("hello");
    });

    // 这个测试是串行执行的 (相对于其他 .concurrent 测试)
    it("test D runs normally", () => {
    expect(0).toBe(0);
    });
    });

6. 超时与异步测试

  • 说明:

    • 异步测试: Vitest 对 async/await 和返回 Promise 的测试有原生支持。只需将测试函数标记为 async 或直接返回一个 Promise。Vitest 会等待 Promise 解决或拒绝。
    • 超时: 每个测试默认有 5000ms (5 秒) 的超时时间。可以通过以下方式修改:
      • CLI 参数: vitest --testTimeout=10000
      • 配置文件: test.testTimeout
      • describeit/test 的第二个参数(一个对象)中设置 timeout 属性。
      • it/test/钩子函数的第三个参数中直接传递超时时间(数字)。
  • 示例代码:

    import { describe, it, expect } from "vitest";

    const fetchData = (shouldSucceed: boolean) =>
    new Promise((resolve, reject) => {
    setTimeout(() => {
    if (shouldSucceed) {
    resolve({ data: "success" });
    } else {
    reject(new Error("fetch failed"));
    }
    }, 100); // 模拟网络请求
    });

    describe("Async and Timeout", () => {
    // 使用 async/await
    it("should fetch data successfully using async/await", async () => {
    const result = await fetchData(true);
    expect(result).toEqual({ data: "success" });
    });

    // 直接返回 Promise
    it("should handle rejected promise", () => {
    // Vitest 会自动处理 Promise 的 reject
    return expect(fetchData(false)).rejects.toThrow("fetch failed");
    });

    // 设置单个测试的超时时间为 200ms
    it("should complete within 200ms", async () => {
    await fetchData(true);
    }, 200); // 第三个参数直接设置超时时间

    // 使用对象配置超时时间
    it(
    "should also complete within 200ms (object config)",
    { timeout: 200 },
    async () => {
    await fetchData(true);
    }
    );

    // 这个测试会因为超时而失败 (默认 5s,这里模拟耗时 6s)
    // it('should fail due to timeout', async () => {
    // await new Promise(resolve => setTimeout(resolve, 6000));
    // }, 5000); // 如果不设置第三个参数,则使用默认或配置的超时
    });

7. 参数化测试(each)

  • 说明: .each 允许你使用不同的输入数据多次运行同一个测试用例,减少代码重复。它有两种主要形式:

    • 数组形式: it.each(table)(name, fn)table 是一个包含测试参数的数组(通常是数组的数组或对象的数组)。
    • 模板字符串形式: it.each \`` ... ` `` (name, fn),使用表格形式的模板字符串定义参数。
  • 示例代码:

    import { describe, it, expect } from "vitest";
    import { add } from "../src/math"; // 假设有 add 函数

    describe("Parameterized Tests with .each", () => {
    // 1. 数组形式 (数组的数组)
    describe("Array of Arrays", () => {
    const testCases: [number, number, number][] = [
    [1, 2, 3],
    [0, 0, 0],
    [-1, 1, 0],
    [10, -5, 5],
    ];

    // %s 会被参数替换 (或使用 %i 索引, %j JSON)
    // testCases 中的每个子数组对应一次测试调用
    it.each(testCases)("add(%s, %s) should return %s", (a, b, expected) => {
    expect(add(a, b)).toBe(expected);
    });
    });

    // 2. 数组形式 (对象的数组)
    describe("Array of Objects", () => {
    const testCases = [
    { a: 1, b: 2, expected: 3, description: "positive numbers" },
    { a: -5, b: -3, expected: -8, description: "negative numbers" },
    { a: 100, b: 0, expected: 100, description: "with zero" },
    ];

    // 使用对象的属性名进行插值
    it.each(testCases)(
    "should handle $description: add($a, $b) => $expected",
    ({ a, b, expected }) => {
    expect(add(a, b)).toBe(expected);
    }
    );
    });

    // 3. 模板字符串形式
    describe("Template Literal", () => {
    // 表格标题行定义变量名,后续行是参数值
    it.each`
    a | b | expected | description
    ${1} | ${2} | ${3} | ${"positive"}
    ${-1} | ${-1} | ${-2} | ${"negative"}
    ${0} | ${5} | ${5} | ${"zero"}
    `(
    "Template: $description - add($a, $b) should be $expected",
    ({ a, b, expected }) => {
    expect(add(a, b)).toBe(expected);
    }
    );
    });
    });

四、Mock 能力 (vi)

Vitest 提供了一个强大的内置 Mocking API,通过全局(如果启用 globals: true)或导入的 vi 对象来访问。它的 API 设计同样与 Jest 非常相似。

1. 自动 Mock 与手动 Mock

  • 说明:

    • 自动 Mock (vi.mock): Vitest 可以自动模拟整个模块。当你 vi.mock('path/to/module') 时,所有从该模块导出的函数都会被替换成行为类似 vi.fn() 的 Mock 函数,并且它们不会执行原始实现。这对于模拟第三方库或项目内部模块非常有用。
    • 手动 Mock: 你可以提供一个工厂函数作为 vi.mock 的第二个参数,来精确控制 Mock 的实现。这允许你为模块的导出提供自定义的 Mock 函数或值。手动 Mock 通常放在 __mocks__ 目录下(与 Jest 类似)或者直接在 vi.mock 的工厂函数中定义。
  • 示例代码:

    // src/userService.ts
    export const getUserById = (id: number) => {
    console.log("Fetching user from DB..."); // 原始实现会打印日志
    return { id, name: `User ${id}` };
    };

    export const defaultUser = { id: 0, name: "Guest" };

    // test/userService.test.ts
    import { describe, it, expect, vi, beforeEach } from "vitest";
    // import * as userService from '../src/userService'; // 在 mock 之后导入

    // 1. 自动 Mock
    vi.mock("../src/userService"); // 告诉 Vitest 自动模拟这个模块

    // 需要在 vi.mock 之后导入被模拟的模块
    import * as userService from "../src/userService";
    // 或者 import { getUserById, defaultUser } from '../src/userService';

    describe("User Service Mocking", () => {
    beforeEach(() => {
    // 在每个测试前重置 mock 状态 (调用次数等)
    vi.clearAllMocks();
    // 如果需要重置 mock 的实现到初始状态(自动 mock 或工厂函数定义的)
    // vi.resetAllMocks();
    });

    it("should automatically mock getUserById", () => {
    // 调用被自动 mock 的函数
    const user = userService.getUserById(1);

    // 它现在是一个 mock 函数,不会执行原始实现(不会打印日志)
    expect(userService.getUserById).toHaveBeenCalled();
    expect(userService.getUserById).toHaveBeenCalledWith(1);

    // 自动 mock 的函数默认返回 undefined
    expect(user).toBeUndefined();

    // 非函数导出(如 defaultUser)不会被自动 mock,保持原样
    // 注意:实际行为可能取决于 Vitest 版本和配置,有时非函数也会被处理
    // 推荐在需要时显式地 mock 或使用工厂函数
    // expect(userService.defaultUser).toEqual({ id: 0, name: 'Guest' });
    });

    // 2. 手动 Mock (使用工厂函数)
    // 在另一个测试文件中或使用 describe 块隔离
    // vi.mock('../src/anotherService', () => ({
    // fetchData: vi.fn(() => Promise.resolve('mocked data')),
    // CONFIG: { timeout: 500 } // 可以 mock 非函数导出
    // }));
    // import { fetchData, CONFIG } from '../src/anotherService';
    // it('should use manual mock implementation', async () => {
    // const data = await fetchData();
    // expect(fetchData).toHaveBeenCalled();
    // expect(data).toBe('mocked data');
    // expect(CONFIG.timeout).toBe(500);
    // });

    // 3. __mocks__ 文件夹 (手动 Mock 的另一种方式)
    // 在项目根目录或 src 下创建 `__mocks__/userService.ts`
    // // __mocks__/userService.ts
    // export const getUserById = vi.fn((id: number) => ({ id, name: `Mock User ${id}` }));
    // export const defaultUser = { id: -1, name: 'Mock Guest' };

    // 然后在测试文件中只需 vi.mock('../src/userService');
    // Vitest 会自动查找并使用 __mocks__ 目录下的实现
    });

2. mock, unmock, doMock, doUnmock, importActual, importMock

  • 说明:

    • vi.mock(path, factory?): 如上所述,用于模拟模块。注意: vi.mock 的调用会被提升 (hoisted) 到文件的顶部执行,无论它写在哪里。这意味着你不能在测试逻辑中间根据条件 vi.mock
    • vi.unmock(path): 取消对指定模块的模拟,恢复使用原始实现。同样会被提升。
    • vi.doMock(path, factory?): 功能与 vi.mock 类似,但它不会被提升。这意味着你可以在 beforeEachit 内部根据需要动态地应用 Mock。这在需要根据不同测试用例使用不同 Mock 实现时非常有用。
    • vi.doUnmock(path): 功能与 vi.unmock 类似,但不会被提升。用于动态取消 Mock。
    • vi.importActual(path): 异步导入模块的原始实现,即使该模块已经被 vi.mock 模拟了。这在你需要在 Mock 工厂函数中引用原始模块的部分功能时很有用。
    • vi.importMock(path): 异步导入模块的模拟版本。通常在你需要在 vi.mock 工厂函数之外获取到 Mock 对象时使用。
  • 示例代码:

    import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";

    describe("Dynamic Mocking and Importing", () => {
    // 使用 doMock 在 beforeEach 中动态模拟
    beforeEach(async () => {
    // 模拟 mathUtils,但在 test 2 中会取消模拟
    await vi.doMock("../src/mathUtils", () => ({
    add: vi.fn((a: number, b: number) => a + b + 100), // 错误的加法
    subtract: vi.fn((a: number, b: number) => a - b),
    }));
    });

    afterEach(() => {
    // 清理动态 mock
    vi.doUnmock("../src/mathUtils");
    // vi.resetModules(); // 重置模块缓存,确保下次 import 拿到最新状态
    });

    it("test 1: should use the dynamically mocked add function", async () => {
    // 导入的是 mock 版本
    const mathUtils = await vi.importMock("../src/mathUtils");
    expect(mathUtils.add(1, 2)).toBe(103); // 1 + 2 + 100
    expect(mathUtils.add).toHaveBeenCalledWith(1, 2);
    });

    it("test 2: should use the original add function after unmocking", async () => {
    // 取消模拟
    vi.doUnmock("../src/mathUtils");
    vi.resetModules(); // 需要重置模块缓存才能拿到原始模块

    // 导入的是原始版本
    const mathUtils = await import("../src/mathUtils"); // 使用原生 import
    // 或者 const mathUtils = await vi.importActual('../src/mathUtils');

    expect(mathUtils.add(1, 2)).toBe(3); // 原始实现
    // expect(mathUtils.add).not.toHaveBeenCalled(); // 原始函数不是 mock 函数
    });

    // 在 mock 工厂中使用 importActual
    it("test 3: mock factory using original implementation", async () => {
    await vi.doMock("../src/complexService", async () => {
    const original = await vi.importActual<
    typeof import("../src/complexService")
    >("../src/complexService");
    return {
    ...original, // 保留原始模块的其他导出
    processData: vi.fn(() => "mocked process result"), // 只 mock processData
    // setup: original.setup // 可以直接引用原始函数
    };
    });

    const complexService = await vi.importMock("../src/complexService");
    expect(complexService.processData()).toBe("mocked process result");
    // expect(complexService.anotherFunction).toBe(original.anotherFunction); // 假设有其他函数
    });
    });

3. spyOn 与模拟函数 (vi.fn)

  • 说明:

    • vi.fn(implementation?): 创建一个空的 Mock 函数,可以追踪调用、参数、返回值和实例。可以提供一个可选的实现函数。
    • vi.spyOn(object, methodName): 监视(spy)一个对象上的现有方法。它会包装原始方法,允许你追踪调用,同时默认情况下仍然执行原始实现。你也可以使用 .mockImplementation().mockReturnValue() 等方法替换其实现。spyOn 创建的 spy 需要手动恢复,通常在 afterEach 中使用 vi.restoreAllMocks()spy.mockRestore()
  • 示例代码:

    import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";

    const calculator = {
    add: (a: number, b: number) => a + b,
    subtract(a: number, b: number) {
    // 使用方法语法
    return a - b;
    },
    };

    describe("Spying and Mock Functions", () => {
    beforeEach(() => {
    // 在每个测试前重置所有 mock/spy 状态
    vi.clearAllMocks();
    });

    afterEach(() => {
    // 恢复所有被 spyOn 改变的方法
    vi.restoreAllMocks();
    });

    it("should create a basic mock function", () => {
    const mockCallback = vi.fn();
    [1, 2].forEach(mockCallback);

    expect(mockCallback).toHaveBeenCalled();
    expect(mockCallback).toHaveBeenCalledTimes(2);
    expect(mockCallback).toHaveBeenCalledWith(1, 0, [1, 2]); // 回调参数
    expect(mockCallback).toHaveBeenNthCalledWith(2, 2, 1, [1, 2]);
    });

    it("should create a mock function with implementation", () => {
    const mockAdd = vi.fn((a, b) => a + b + 10); // 带实现的 mock
    expect(mockAdd(3, 4)).toBe(17);
    expect(mockAdd).toHaveReturnedWith(17);
    });

    it("should spy on an object method and track calls", () => {
    // 监视 calculator.add 方法
    const addSpy = vi.spyOn(calculator, "add");

    const result = calculator.add(5, 3); // 调用原始方法

    expect(result).toBe(8); // 原始实现被执行
    expect(addSpy).toHaveBeenCalled();
    expect(addSpy).toHaveBeenCalledWith(5, 3);
    expect(addSpy).toHaveReturnedWith(8);

    // addSpy.mockRestore(); // 单独恢复 spy
    });

    it("should spy on and mock the implementation of a method", () => {
    const subtractSpy = vi.spyOn(calculator, "subtract");
    // 替换实现
    subtractSpy.mockImplementation((a, b) => a - b + 100);

    const result = calculator.subtract(10, 5); // 调用的是 mock 实现

    expect(result).toBe(105); // 10 - 5 + 100
    expect(subtractSpy).toHaveBeenCalledWith(10, 5);
    expect(subtractSpy).toHaveReturnedWith(105);

    // 恢复原始实现
    subtractSpy.mockRestore();
    expect(calculator.subtract(10, 5)).toBe(5); // 恢复后执行原始实现
    });

    it("should spy on and mock the return value", () => {
    const addSpy = vi.spyOn(calculator, "add");
    addSpy.mockReturnValue(999); // 固定返回值

    expect(calculator.add(1, 1)).toBe(999); // 返回固定值
    expect(addSpy).toHaveBeenCalledWith(1, 1);
    });
    });

4. 定时器与日期 Mock

  • 说明: Vitest 提供了控制 setTimeout, setInterval, Date 等时间相关 API 的能力,使得测试依赖时间的代码变得可预测。

    • vi.useFakeTimers(): 启用定时器 Mock。所有定时器调用将被 Vitest 控制。
    • vi.useRealTimers(): 禁用定时器 Mock,恢复使用真实定时器。
    • vi.advanceTimersByTime(ms): 快进时间,并执行在此期间到期的所有定时器。
    • vi.advanceTimersToNextTimer(): 快进到下一个定时器即将执行的时间点,并执行它。
    • vi.runAllTimers(): 执行当前队列中所有待处理的宏任务(setTimeout, setInterval)定时器。
    • vi.runAllTicks(): 执行当前队列中所有待处理的微任务(Promise.then)。
    • vi.setSystemTime(date | timestamp): 设置当前的系统时间 (Date.now(), new Date())。
    • vi.getRealSystemTime() / vi.getTimerCount(): 获取真实时间 / 当前待处理的定时器数量。
  • 注意: 通常在 beforeEachvi.useFakeTimers(),在 afterEachvi.useRealTimers()

  • 示例代码:

    import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";

    function runAfterDelay(callback: () => void, delay: number) {
    setTimeout(callback, delay);
    }

    function getCurrentYear() {
    return new Date().getFullYear();
    }

    describe("Timer and Date Mocking", () => {
    beforeEach(() => {
    vi.useFakeTimers(); // 启用 Mock 定时器和 Date
    });

    afterEach(() => {
    vi.useRealTimers(); // 恢复真实定时器和 Date
    });

    it("should execute setTimeout callback after advancing time", () => {
    const myCallback = vi.fn();
    runAfterDelay(myCallback, 1000); // 设置 1 秒后执行的回调

    expect(myCallback).not.toHaveBeenCalled(); // 时间未到,回调未执行

    vi.advanceTimersByTime(500); // 快进 500ms
    expect(myCallback).not.toHaveBeenCalled();

    vi.advanceTimersByTime(500); // 再快进 500ms (总共 1000ms)
    expect(myCallback).toHaveBeenCalledTimes(1); // 回调执行
    });

    it("should run all timers", () => {
    const cb1 = vi.fn();
    const cb2 = vi.fn();
    setTimeout(cb1, 100);
    setTimeout(cb2, 200);

    vi.runAllTimers(); // 执行所有待处理定时器

    expect(cb1).toHaveBeenCalled();
    expect(cb2).toHaveBeenCalled();
    });

    it("should mock the system date", () => {
    const specificDate = new Date(2023, 5, 15); // 2023年6月15日 (月份从0开始)
    vi.setSystemTime(specificDate);

    expect(getCurrentYear()).toBe(2023);
    expect(new Date().getDate()).toBe(15);
    expect(Date.now()).toBe(specificDate.getTime());

    // 快进时间也会影响 Date.now()
    vi.advanceTimersByTime(24 * 60 * 60 * 1000); // 快进一天
    expect(new Date().getDate()).toBe(16);
    });
    });

5. 网络请求与全局对象 Mock

  • 说明:

    • 网络请求: Vitest 本身不内置网络请求 Mock 库,但推荐使用如 msw (Mock Service Worker) 或 nock (Node.js HTTP mocking) 等库。你可以将这些库的设置集成到 Vitest 的 setupFiles 中。
    • 全局对象: 对于需要 Mock 的全局对象(如 window, document - 在 'jsdom'/'happy-dom' 环境下,或者自定义的全局变量),可以使用 vi.spyOn 配合 getter 来模拟其属性,或者直接在测试文件的顶部(或 setupFiles)修改全局对象。
  • 示例代码:

    // setupTests.ts (在 vitest.config.ts 中配置 setupFiles: ['./setupTests.ts'])
    import { vi } from "vitest";
    import { server } from "./mocks/server"; // 假设使用 msw

    // 启动 msw 服务器
    beforeAll(() => server.listen({ onUnhandledRequest: "error" }));
    // 每个测试后重置 msw 处理程序
    afterEach(() => server.resetHandlers());
    // 所有测试结束后关闭服务器
    afterAll(() => server.close());

    // 模拟全局 fetch (如果未使用 msw 等拦截库)
    // const mockFetch = vi.fn(() => Promise.resolve({
    // ok: true,
    // json: () => Promise.resolve({ data: 'mocked fetch response' })
    // }));
    // vi.stubGlobal('fetch', mockFetch); // 使用 stubGlobal 模拟全局变量

    // test/api.test.ts
    import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
    import { fetchUserData } from "../src/apiService"; // 假设这个函数内部使用了 fetch

    // 模拟 window.location
    describe("Global Object Mocking", () => {
    const originalLocation = window.location;

    beforeEach(() => {
    // 需要确保能够修改 window.location
    // Vitest 的 jsdom/happy-dom 环境通常允许这样做
    // 使用 Object.defineProperty 来模拟只读属性
    Object.defineProperty(window, "location", {
    writable: true, // 使其可写,以便我们可以更改它
    value: {
    ...originalLocation, // 保留原始 location 的其他部分
    href: "http://test.com/path",
    pathname: "/path",
    search: "?query=123",
    assign: vi.fn(), // mock assign 方法
    },
    });
    });

    afterEach(() => {
    // 恢复原始 location
    Object.defineProperty(window, "location", {
    writable: true,
    value: originalLocation,
    });
    vi.restoreAllMocks(); // 清理 vi.fn()
    });

    it("should read the mocked location.pathname", () => {
    expect(window.location.pathname).toBe("/path");
    });

    it("should track calls to location.assign", () => {
    window.location.assign("http://new-url.com");
    expect(window.location.assign).toHaveBeenCalledWith("http://new-url.com");
    });
    });

    // 测试网络请求 (假设已通过 setupTests.ts 配置了 msw)
    describe("API Service with MSW", () => {
    it("fetchUserData should return mocked data", async () => {
    // msw 会拦截实际的 fetch 调用并返回 mock 响应
    const userData = await fetchUserData(1);
    expect(userData).toEqual({ id: 1, name: "Mocked User via MSW" });
    });
    });

五、测试覆盖率

1. 覆盖率统计(coverage)

  • 说明: Vitest 支持生成代码覆盖率报告,以评估测试对代码库的覆盖程度。它可以通过 CLI 参数或配置文件启用。

  • 启用:

    • CLI: vitest run --coverage

    • 配置文件 (vitest.config.ts):

      import { defineConfig } from "vitest/config";

      export default defineConfig({
      test: {
      coverage: {
      enabled: true, // 启用覆盖率
      // provider: 'v8' (默认) 或 'istanbul'
      },
      },
      });
  • Provider: Vitest 支持两种覆盖率引擎:

    • v8: (默认) Node.js 内置的覆盖率引擎,速度快,但可能在某些边缘情况下(如 TS 装饰器、复杂类型转换)不如 Istanbul 精确。需要 Node.js v14.18+。
    • istanbul: 成熟的、广泛使用的覆盖率工具链,兼容性好,功能更全面,但通常比 v8 慢。

2. 配置覆盖率报告格式(text、html、lcov 等)

  • 说明: 可以配置生成多种格式的覆盖率报告。
  • 配置 (vitest.config.tstest.coverage 对象内):
    coverage: {
    enabled: true,
    provider: 'istanbul', // 或 'v8'
    // 配置报告器
    reporter: [
    // 输出到控制台的文本摘要
    'text',
    // 生成详细的 HTML 报告,可在浏览器中查看
    'html',
    // 生成 lcov.info 文件,常用于 CI 服务 (如 Coveralls, Codecov)
    'lcov',
    // 生成 cobertura 格式的 XML 报告,常用于 Jenkins 等 CI
    'cobertura',
    // 生成 JSON 格式的报告
    'json',
    // 生成 JSON 摘要
    'json-summary',
    ],
    // 配置各种报告器的输出目录
    reportsDirectory: './coverage', // 默认是 ./coverage
    }

3. 排除文件/目录

  • 说明: 通常需要从覆盖率报告中排除测试文件、配置文件、类型定义文件、第三方库或某些特定目录。
  • 配置 (vitest.config.tstest.coverage 对象内):
    coverage: {
    enabled: true,
    provider: 'v8',
    // 使用 glob 模式指定包含哪些文件进行覆盖率统计
    include: ['src/**/*.{js,ts,jsx,tsx}'],
    // 使用 glob 模式指定排除哪些文件
    exclude: [
    'node_modules/**',
    'dist/**',
    'coverage/**',
    '*.config.{js,ts,cjs,mjs}', // 排除配置文件
    '**/*.d.ts', // 排除类型定义文件
    'src/main.ts', // 排除入口文件 (如果不需要)
    'src/types/**', // 排除类型目录
    '**/{tests,test,__tests__}/**', // 排除测试文件目录
    '**/*.{test,spec}.{js,ts,jsx,tsx}', // 排除测试文件本身
    'src/mocks/**', // 排除 mock 文件目录
    'src/**/*.stories.{js,ts,jsx,tsx}', // 排除 Storybook 文件
    ],
    // 如果 all 设置为 true, 未被测试覆盖的文件也会包含在报告中 (显示 0% 覆盖率)
    all: true,
    // 清理上次运行的文件 (推荐开启)
    clean: true,
    }

4. 与 CI 集成

  • 说明: 将覆盖率报告集成到持续集成 (CI) 流程中,可以在每次代码提交或合并时自动检查覆盖率。通常需要将覆盖率报告(如 lcov.info)上传到第三方服务(如 Codecov, Coveralls)或 CI 平台自身的功能。
  • 步骤:
    1. 在 CI 脚本中运行覆盖率: 修改 CI 配置文件(如 .github/workflows/ci.yml, .gitlab-ci.yml)中的测试步骤,添加 --coverage 参数。
      # GitHub Actions 示例
      jobs:
      test:
      runs-on: ubuntu-latest
      steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-node@v3
      with:
      node-version: "18"
      cache: "pnpm" # 或 npm/yarn
      - run: pnpm install # 或 npm ci / yarn install
      - name: Run tests and collect coverage
      run: pnpm test --coverage # 或 npm run test -- --coverage
      - name: Upload coverage reports to Codecov
      uses: codecov/codecov-action@v3
      with:
      # token: ${{ secrets.CODECOV_TOKEN }} # 如果是私有仓库需要 token
      files: ./coverage/lcov.info # 指定 lcov 文件路径
      fail_ci_if_error: true
    2. 配置覆盖率报告格式: 确保 vitest.config.ts 中配置了 CI 服务所需的报告格式(通常是 lcov)。
    3. 设置覆盖率阈值 (可选): 可以在 vitest.config.tscoverage.thresholds 中设置覆盖率阈值(行、分支、函数、语句)。如果未达到阈值,Vitest 会以非零状态码退出,从而使 CI 构建失败。
      coverage: {
      // ... 其他配置
      thresholds: {
      lines: 80,
      functions: 70,
      branches: 70,
      statements: 80,
      // 也可以为全局或单个文件设置阈值
      // global: { ... },
      // './src/utils/critical.ts': { lines: 95 }
      },
      // 如果阈值检查失败是否以错误退出 (默认为 false)
      // 在 CI 中通常需要设置为 true
      // failUnderThresholds: true, // Vitest v1+ 可能不再需要这个,检查失败默认会退出
      }
    4. 配置 CI 服务: 根据所选的覆盖率服务(Codecov, Coveralls 等)或 CI 平台的要求,可能需要在项目中添加配置文件(如 codecov.yml)或在 CI 服务网站上进行设置。

六、快照测试

1. 快照基础(toMatchSnapshottoMatchInlineSnapshot

  • 说明: 快照测试是一种用于验证 UI 组件、数据结构或函数输出是否随时间保持不变的技术。

    • 当你第一次运行 expect(value).toMatchSnapshot() 时,Vitest 会将 value 的序列化结果存储在一个 .snap 文件中(通常在 __snapshots__ 目录下)。
    • 之后每次运行测试,Vitest 会将当前的 value 序列化后与存储的快照进行比较。如果匹配,测试通过;如果不匹配,测试失败,提示你需要更新快照或修复代码。
    • toMatchInlineSnapshot() 功能类似,但它会将快照内容直接写入测试文件 (.test.ts) 中 expect 语句的参数里,而不是单独的 .snap 文件。这对于小型、易读的快照很方便。
  • 适用场景:

    • 测试 React/Vue/Svelte 组件的渲染输出。
    • 验证复杂的对象或数据结构。
    • 确保 API 响应格式的稳定性。
  • 示例代码:

    import { describe, it, expect } from "vitest";

    function getUserProfile(userId: number) {
    // 模拟返回一个复杂的对象
    return {
    id: userId,
    name: `User ${userId}`,
    email: `user${userId}@example.com`,
    address: {
    street: `${userId} Main St`,
    city: "Anytown",
    zip: `${userId}0000`,
    },
    createdAt: new Date("2023-01-01T10:00:00.000Z"), // 注意 Date 对象
    roles: ["user", userId === 1 ? "admin" : undefined].filter(Boolean), // 动态角色
    settings: {
    theme: "dark",
    notifications: {
    email: true,
    sms: false,
    },
    },
    };
    }

    describe("Snapshot Testing", () => {
    it("should match the user profile snapshot", () => {
    const userProfile = getUserProfile(1);
    // 第一次运行时会生成 __snapshots__/myTest.test.ts.snap 文件
    // 后续运行会与 .snap 文件内容比较
    expect(userProfile).toMatchSnapshot();
    });

    it("should match the user profile snapshot for another user", () => {
    const userProfile = getUserProfile(2);
    // 同一个 describe 块但不同的 it, 会在同一个 .snap 文件中生成独立的快照
    expect(userProfile).toMatchSnapshot();
    });

    it("should match the theme setting inline snapshot", () => {
    const settings = getUserProfile(3).settings;
    // 第一次运行时,Vitest 会自动填充快照内容到括号里
    // 后续运行会与括号里的内容比较
    expect(settings.theme).toMatchInlineSnapshot('"dark"'); // 假设第一次运行后填充了 "dark"
    });

    it("should match the notification settings inline snapshot", () => {
    const notifications = getUserProfile(4).settings.notifications;
    // 对于对象,内联快照会格式化
    expect(notifications).toMatchInlineSnapshot(`
    {
    "email": true,
    "sms": false,
    }
    `);
    });

    // 使用属性匹配器进行部分快照
    it("should match parts of the profile using property matchers", () => {
    const userProfile = getUserProfile(5);
    expect(userProfile).toMatchSnapshot({
    // 对于动态变化或不关心的字段,可以使用 expect.any() 或具体类型
    id: expect.any(Number), // id 只要是数字就行
    createdAt: expect.any(Date), // createdAt 只要是 Date 对象就行
    address: {
    zip: expect.stringMatching(/^\d{5}$/), // zip 必须是5位数字
    },
    });
    });
    });

    // 生成的 __snapshots__/myTest.test.ts.snap 文件可能看起来像这样:
    /*
    // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html

    exports[`Snapshot Testing > should match the user profile snapshot 1`] = `
    {
    "address": {
    "city": "Anytown",
    "street": "1 Main St",
    "zip": "10000",
    },
    "createdAt": 2023-01-01T10:00:00.000Z,
    "email": "user1@example.com",
    "id": 1,
    "name": "User 1",
    "roles": [
    "user",
    "admin",
    ],
    "settings": {
    "notifications": {
    "email": true,
    "sms": false,
    },
    "theme": "dark",
    },
    }
    `;

    exports[`Snapshot Testing > should match the user profile snapshot for another user 1`] = `
    {
    "address": {
    // ... user 2 data ...
    },
    // ... rest of user 2 profile ...
    }
    `;
    // ... 其他快照 ...
    */

2. 快照更新与管理

  • 说明: 当代码变更导致快照不再匹配时(无论是预期的变更还是 Bug),测试会失败。此时需要:
    1. 审查差异: Vitest 会在控制台清晰地展示新旧快照之间的差异。仔细检查这些差异是否符合预期。
    2. 更新快照: 如果差异是预期的(例如,你修改了组件的输出或数据结构),运行 vitest -uvitest --update (或者在监听模式下按 u 键)。Vitest 会用新的结果覆盖 .snap 文件或更新内联快照。
    3. 修复代码: 如果差异是意外的(引入了 Bug),则需要修复代码,然后重新运行测试,直到快照匹配。
  • 管理:
    • 快照文件 (.snap) 应与测试文件一起提交到版本控制系统(如 Git)。
    • 定期审查快照文件,特别是大型快照,确保它们仍然有意义且易于理解。
    • 对于容易频繁变动或包含大量无关细节的快照,考虑使用更具体的断言或属性匹配器 (expect.any, expect.stringContaining 等) 来代替完整的快照。

3. 快照测试的最佳实践

  • 保持快照简洁: 快照应尽可能小而聚焦。避免对包含大量无关数据或易变数据(如随机 ID、时间戳)的整个大型对象进行快照。使用属性匹配器 (expect.any(), expect.stringMatching()) 或只对对象的关键部分进行快照。
  • 明确测试意图: 快照本身不能完全替代描述性的测试。确保 it/test 的名称清晰地说明了快照要验证的内容。
  • 代码审查: 在代码审查中应仔细检查快照文件的变更。快照更新不应被随意接受,需要确认变更是合理且预期的。
  • 避免过度使用: 不要滥用快照测试所有东西。对于简单的逻辑或可以通过常规断言轻松验证的行为,优先使用常规断言,它们通常更易读、更稳定。快照最适合用于验证结构复杂、序列化输出稳定的场景。
  • 结合其他测试: 快照测试主要验证“输出是否与上次相同”,但不保证输出的正确性。应结合其他单元测试来验证功能的逻辑正确性。
  • 内联快照适用性: 内联快照 (toMatchInlineSnapshot) 适用于非常小、易读的快照(如短字符串、小对象)。对于大型或复杂的结构,独立的 .snap 文件通常更易于管理和审查。

七、测试环境

Vitest 允许你在不同的环境中运行测试,以模拟代码最终运行的场景。

1. jsdom 环境

  • 说明: jsdom 是一个在 Node.js 中模拟浏览器环境(包括 DOM、BOM、windowdocument 等 API)的库。这是进行前端组件单元测试或集成测试时最常用的环境,因为它允许你渲染组件、操作 DOM 和测试与浏览器 API 相关的逻辑。

  • 配置: 在 vitest.config.ts 中设置 environment'jsdom'

  • 示例代码:

    // vitest.config.ts
    import { defineConfig } from "vitest/config";

    export default defineConfig({
    test: {
    environment: "jsdom", // 设置测试环境为 jsdom
    // ... 其他配置
    },
    });

    // test/dom.test.ts
    import { describe, it, expect } from "vitest";

    describe("DOM Manipulation in jsdom environment", () => {
    it("should create and manipulate a DOM element", () => {
    // 在 jsdom 环境下可以直接使用 document
    const div = document.createElement("div");
    div.textContent = "Hello Vitest";
    document.body.appendChild(div);

    const element = document.querySelector("div");
    expect(element).not.toBeNull();
    expect(element?.textContent).toBe("Hello Vitest");

    // 清理 DOM (虽然 jsdom 会在每个测试后重置,但手动清理是好习惯)
    document.body.removeChild(div);
    });
    });

2. node 环境

  • 说明: 这是 Vitest 的默认环境。测试代码直接在 Node.js 运行时中执行。适用于测试与 DOM 无关的逻辑,例如:

    • Node.js 后端代码
    • 纯 JavaScript/TypeScript 工具函数库
    • 算法或数据结构
    • 不涉及浏览器特定 API 的业务逻辑 这个环境通常比 jsdomhappy-dom 更快,因为它不需要模拟浏览器环境。
  • 配置: 在 vitest.config.ts 中设置 environment'node' 或不设置 (使用默认值)。

  • 示例代码:

    // vitest.config.ts
    import { defineConfig } from "vitest/config";

    export default defineConfig({
    test: {
    environment: "node", // 明确设置为 node 环境
    },
    });

    // test/math.test.ts
    import { describe, it, expect } from "vitest";
    import { sum } from "../src/utils/math"; // 假设有一个纯工具函数

    describe("Math Utils in node environment", () => {
    it("should sum two numbers correctly", () => {
    // 不需要 DOM API
    expect(sum(2, 3)).toBe(5);
    });
    });

3. happy-dom 支持

  • 说明: happy-domjsdom 的另一个替代品,旨在提供一个更快、内存占用更低的浏览器环境模拟。它可能没有 jsdom 那么完整或严格遵循 Web 标准,但在许多场景下足够使用,并且可以显著提升测试速度。

  • 配置:

    1. 安装 happy-dom: npm install -D happy-dompnpm add -D happy-domyarn add -D happy-dom
    2. vitest.config.ts 中设置 environment'happy-dom'
  • 示例代码:

    // vitest.config.ts
    import { defineConfig } from "vitest/config";

    export default defineConfig({
    test: {
    environment: "happy-dom", // 设置测试环境为 happy-dom
    },
    });

    // test/component.happy.test.ts (测试代码与 jsdom 类似)
    import { describe, it, expect } from "vitest";

    describe("Component in happy-dom", () => {
    it("should render correctly", () => {
    document.body.innerHTML = "<button>Click Me</button>";
    const button = document.querySelector("button");
    expect(button?.textContent).toBe("Click Me");
    document.body.innerHTML = ""; // 清理
    });
    });

4. 自定义环境

  • 说明: 对于特殊需求(例如测试 Service Workers、Cloudflare Workers、或其他非标准 JavaScript 运行时),Vitest 允许你创建和使用自定义环境。这需要实现 Vitest 提供的 Environment 接口。这是一个高级功能。

  • 配置: 在 vitest.config.ts 中设置 environment 为你自定义环境包的名称或路径。

  • 示例代码:

    // vitest.config.ts
    import { defineConfig } from "vitest/config";

    export default defineConfig({
    test: {
    // environment: 'vitest-environment-my-custom-env', // 指向npm包
    // environment: './path/to/my-custom-environment.ts' // 指向本地文件
    // ...
    },
    });

    // my-custom-environment.ts (极其简化的示意)
    // import type { Environment } from 'vitest';
    // export default <Environment>{
    // name: 'my-custom-environment',
    // async setup() {
    // console.log('Setting up custom environment...');
    // // 设置全局变量、模拟 API 等
    // globalThis.myCustomGlobal = {};
    // return {
    // async teardown() {
    // console.log('Tearing down custom environment...');
    // // 清理资源
    // },
    // };
    // },
    // // 可能还需要实现 handle Vite specific requests 等方法
    // };

    注意: 实现自定义环境比较复杂,具体请参考 Vitest 官方文档。

5. DOM 测试与 UI 测试

  • 说明: 这是指在模拟 DOM 环境 (jsdomhappy-dom) 中进行的测试,主要目的是验证代码是否能正确地与 DOM 交互或 UI 组件是否按预期渲染和响应。Vitest 提供了运行这些测试的基础设施(环境和测试运行器),但具体的 DOM 查询、事件触发和断言通常需要借助专门的库,最常用的是 Testing Library。

  • 示例代码 (概念性,结合 Testing Library 会更具体):

    // test/ui.test.ts (假设在 jsdom 环境)
    import { describe, it, expect } from "vitest";

    describe("Basic UI interaction", () => {
    it("should update text content on button click", () => {
    document.body.innerHTML = `
    <p>Initial Text</p>
    <button>Change Text</button>
    `;
    const paragraph = document.querySelector("p");
    const button = document.querySelector("button");

    expect(paragraph?.textContent).toBe("Initial Text");

    // 模拟点击事件
    button?.addEventListener("click", () => {
    if (paragraph) {
    paragraph.textContent = "Text Changed!";
    }
    });
    button?.click(); // 触发点击

    expect(paragraph?.textContent).toBe("Text Changed!");

    document.body.innerHTML = ""; // 清理
    });
    });

八、与前端框架集成

Vitest 因为与 Vite 的紧密集成,能够非常方便地测试使用 Vite 构建的各种前端框架项目。

1. Vue 组件测试 (@testing-library/vue)

  • 说明: 使用 @testing-library/vue 可以在 Vitest 环境中轻松地测试 Vue 组件。它鼓励你像用户一样与组件交互(查找元素、触发事件)而不是依赖组件内部状态。需要 jsdomhappy-dom 环境。确保你的 Vite 配置包含了 @vitejs/plugin-vue

  • 安装: npm install -D @testing-library/vue @vue/test-utils (Vue Test Utils 是 Testing Library for Vue 的 peer dependency)

  • 示例代码:

    // src/components/Counter.vue
    <template>
    <div>
    <p>Count: {{ count }}</p>
    <button @click="increment">Increment</button>
    </div>
    </template>

    <script setup lang="ts">
    import { ref } from 'vue';
    const count = ref(0);
    const increment = () => count.value++;
    </script>

    // test/Counter.test.ts (确保环境是 jsdom/happy-dom)
    import { describe, it, expect } from 'vitest';
    import { render, screen, fireEvent } from '@testing-library/vue';
    import Counter from '../src/components/Counter.vue';

    describe('Counter.vue', () => {
    it('renders initial count and increments on click', async () => {
    // 1. 渲染组件
    render(Counter);

    // 2. 查找元素 (Testing Library 推荐按角色、文本等查找)
    const countDisplay = screen.getByText(/Count: 0/i); // 使用正则,不区分大小写
    const button = screen.getByRole('button', { name: /Increment/i });

    // 3. 断言初始状态
    expect(countDisplay).toBeInTheDocument(); // 来自 @testing-library/jest-dom (需额外安装和设置) 或自定义匹配器

    // 4. 模拟用户交互
    await fireEvent.click(button);

    // 5. 断言更新后的状态
    // Vue 的更新是异步的,Testing Library 的 findBy* 会自动等待
    const updatedCountDisplay = await screen.findByText(/Count: 1/i);
    expect(updatedCountDisplay).toBeInTheDocument();

    // 再次点击
    await fireEvent.click(button);
    expect(await screen.findByText(/Count: 2/i)).toBeInTheDocument();
    });
    });

    // 可能需要在 setupTests.ts 中扩展 expect (使用 @testing-library/jest-dom)
    // import '@testing-library/jest-dom/vitest';

2. React 组件测试 (@testing-library/react)

  • 说明: 类似地,@testing-library/react 用于测试 React 组件。Vitest 通过 Vite 天然支持 JSX。同样需要 jsdomhappy-dom 环境。

  • 安装: npm install -D @testing-library/react @testing-library/jest-dom

  • 示例代码:

    // src/components/Greeting.jsx (或 .tsx)
    import React, { useState } from "react";

    function Greeting({ initialName = "World" }) {
    const [name, setName] = useState(initialName);
    return (
    <div>
    <p>Hello, {name}!</p>
    <input
    type="text"
    aria-label="Name Input"
    value={name}
    onChange={(e) => setName(e.target.value)}
    />
    </div>
    );
    }
    export default Greeting;

    // test/Greeting.test.jsx (或 .tsx)
    import React from "react";
    import { describe, it, expect } from "vitest";
    import { render, screen, fireEvent } from "@testing-library/react";
    import Greeting from "../src/components/Greeting";
    // import '@testing-library/jest-dom/vitest'; // 确保在 setup file 中导入

    describe("Greeting.jsx", () => {
    it("renders greeting and updates name on input change", async () => {
    render(<Greeting initialName="Vitest" />);

    // 查找元素
    const greetingText = screen.getByText(/Hello, Vitest!/i);
    const nameInput = screen.getByLabelText(/Name Input/i); // 按 aria-label 查找

    expect(greetingText).toBeInTheDocument();
    expect(nameInput).toHaveValue("Vitest");

    // 模拟输入事件
    await fireEvent.change(nameInput, { target: { value: "React" } });

    // 断言更新
    expect(screen.getByText(/Hello, React!/i)).toBeInTheDocument();
    expect(nameInput).toHaveValue("React");
    });
    });

3. Svelte、Solid 等支持

  • 说明: Testing Library 为许多主流框架都提供了相应的包,如 @testing-library/svelte, @testing-library/solid。使用方式与 Vue/React 类似:安装对应的库,在 jsdom/happy-dom 环境下使用 render 函数挂载组件,然后通过 screen 查询和 fireEvent 交互。具体用法请参考各库的文档。Vitest + Vite 的组合通常能很好地处理这些框架的编译和测试。

4. 组件挂载与渲染

  • 说明: 在框架测试中,“挂载”或“渲染”指的是将组件实例创建并附加到测试环境的模拟 DOM 中。Testing Library 的 render 函数负责处理这个过程,它不仅渲染组件,还返回一些用于调试和查询的工具函数(尽管推荐优先使用全局的 screen 对象进行查询)。

5. 测试库(Testing Library)集成

  • 说明: Testing Library 是目前社区推荐的 UI 测试实践库。它的核心理念是像用户一样测试:通过用户可见或可交互的方式(如文本内容、标签、ARIA 角色)来查找元素,通过模拟用户事件(点击、输入)来驱动组件,然后断言用户可见的结果。它避免了测试组件的内部实现细节(如 state、props、方法调用),使得测试更加健壮,不易因重构而失败。
  • 核心 API:
    • render(): 渲染组件。
    • screen: 提供查询方法(getBy*, findBy*, queryBy*)在整个 document.body 中查找元素。
    • fireEvent: 触发 DOM 事件(click, change, submit 等)。
    • waitFor: 等待某个异步断言变为真。
  • 与 Vitest 结合: Vitest 提供测试运行环境和基础断言,Testing Library 提供组件渲染、交互和查询能力。通常还需要 @testing-library/jest-dom 来提供更方便的 DOM 相关断言(如 toBeInTheDocument, toHaveClass, toHaveValue 等),需要在 setupTests.ts 中引入并扩展 expect

九、调试与开发体验 (DX)

Vitest 提供了多种功能来提升测试的开发和调试效率。

1. watch 模式

  • 说明: 运行 vitest 命令(不带 run 参数)会启动监听模式。Vitest 会监视项目文件(源文件和测试文件)的变化。当检测到文件更改时,它会自动重新运行相关的测试文件,提供即时反馈。这是 Vitest 提升开发效率的关键特性之一。
  • 交互式命令: 在 watch 模式下,可以按键盘快捷键进行交互:
    • f: 切换文件名过滤模式,输入文件名或模式只运行匹配的测试。
    • t: 切换测试名称过滤模式,输入测试名称 (describeit 的名称) 只运行匹配的测试。
    • u: 更新失败的快照。
    • r: 重新运行所有测试。
    • q: 退出 watch 模式。
  • 示例:
    # 启动 watch 模式
    pnpm vitest
    # 或者 npm run test (如果 script 是 "vitest")
    # 或者 yarn test
    (控制台会显示菜单提示可用的快捷键)

2. 测试过滤与定位

  • 说明: 除了 watch 模式下的交互式过滤,还可以通过多种方式精确地运行你关心的测试:

    • CLI 文件路径: vitest src/components/Button.test.ts 只运行指定文件。
    • CLI 文件名模式: vitest button 会运行文件名包含 button 的所有测试文件。
    • CLI 测试名称: vitest -t "should render correctly"vitest --test-name-pattern "should render" 只运行 it/test 名称匹配的测试用例。
    • 代码内标记: 在 describeit/test 上使用 .only 修饰符,如 it.only(...)。运行 vitest 时,只有标记了 .only 的测试会执行(如果存在多个 .only,它们都会执行)。
  • 示例:

    # 只运行 Button.test.ts
    vitest src/components/Button.test.ts

    # 只运行名字包含 "login" 的测试用例
    vitest -t login

    # 运行文件名包含 utils 且测试名包含 "edge case" 的测试
    vitest utils -t "edge case"
    // 只运行这个测试用例
    it.only("this specific test case will run", () => {
    // ...
    });

    // 只运行这个 describe 块中的所有测试
    describe.only("Focused Test Suite", () => {
    it("test A", () => {});
    it("test B", () => {});
    });

3. 错误追踪与调试

  • 说明: 当测试失败时,Vitest 会提供清晰的错误报告:
    • 失败断言: 显示期望值 (Expected) 和实际值 (Received) 的差异 (diff)。
    • 快照不匹配: 显示新旧快照内容的差异。
    • 代码错误: 提供错误的堆栈跟踪 (Stack Trace),指出错误发生在哪个文件的哪一行。
  • 调试方法:
    • console.log: 在测试代码或被测代码中添加 console.log 语句,其输出会显示在测试运行结果中。
    • debugger: 在代码中添加 debugger; 语句。然后使用 Node.js 的调试器运行 Vitest。
      # 使用 Node inspect 运行 Vitest
      node --inspect-brk ./node_modules/vitest/vitest.mjs run <test_file>
      然后使用 Chrome DevTools (访问 chrome://inspect) 或 VSCode 的 JavaScript Debug Terminal 连接到 Node.js 进程进行调试。
    • IDE 调试: 使用 VSCode 等 IDE 的调试功能(见下文)。

4. VSCode 插件与断点调试

  • 说明: 为了更方便地调试,推荐安装官方的 Vitest VSCode Extension

    • 特性:
      • 在编辑器侧边栏(Gutter)显示运行/调试测试的按钮。
      • 在“测试”面板中查看和管理测试套件及结果。
      • 允许直接在 .ts/.js/.vue 等测试文件中设置断点,然后通过点击 Gutter 中的调试按钮启动调试会话。
  • 配置 (.vscode/launch.json): 虽然插件通常能处理好调试启动,但有时你可能需要自定义调试配置。

  • 示例 (launch.json):

    {
    "version": "0.2.0",
    "configurations": [
    {
    "type": "node",
    "request": "launch",
    "name": "Vitest Current File", // 调试当前打开的测试文件
    "program": "${workspaceFolder}/node_modules/vitest/vitest.mjs",
    "args": [
    "run",
    "${relativeFile}", // 传入当前文件的相对路径
    "--config", // 指定配置文件(如果需要)
    "${workspaceFolder}/vitest.config.ts"
    ],
    "smartStep": true,
    "skipFiles": ["<node_internals>/**", "**/node_modules/**"],
    "console": "integratedTerminal",
    "internalConsoleOptions": "neverOpen"
    },
    {
    "type": "node",
    "request": "launch",
    "name": "Vitest All", // 调试所有测试
    "program": "${workspaceFolder}/node_modules/vitest/vitest.mjs",
    "args": [
    "run", // 或者 "watch" 模式进行调试
    "--config",
    "${workspaceFolder}/vitest.config.ts"
    ],
    "smartStep": true,
    "skipFiles": ["<node_internals>/**", "**/node_modules/**"],
    "console": "integratedTerminal",
    "internalConsoleOptions": "neverOpen"
    }
    ]
    }

    配置好后,可以在 VSCode 的“运行和调试”面板选择相应的配置来启动调试。

5. 输出美化与日志

  • 说明: Vitest 默认提供彩色、结构化的控制台输出,清晰地显示测试进度、通过/失败状态和错误信息。测试代码或源文件中使用的 console.log 等日志语句会正常输出到控制台,方便在测试运行时观察内部状态。
  • 注意: 虽然 console.log 对于临时调试很有用,但通常建议在最终提交的代码和测试中移除不必要的日志语句,保持测试输出的干净。对于更复杂的日志需求,可以考虑使用专门的日志库,并可能需要配置 Vitest 或测试环境来处理日志输出(例如,在测试期间抑制日志或重定向到文件)。

十、性能与并发

1. 并发测试与隔离

  • 说明:

    • 并发 (.concurrent): 如前所述,.concurrent 允许同一文件内的测试用例并行执行(在同一工作线程内),适合 I/O 密集型测试。
    • 隔离: Vitest 默认情况下会尽可能地隔离测试文件。它使用工作线程池 (worker threads) 或子进程 (forks) 来并行运行不同的测试文件(可通过 poolpoolOptions 配置)。这意味着不同文件间的全局状态通常是隔离的。然而,使用了 .concurrent 的同一文件内的测试用例则共享同一个上下文和全局状态,需要开发者自行确保这些并发测试不会相互干扰(例如,避免修改共享的 mock、全局变量或 DOM 状态)。
  • 示例代码 (并发):

    import { describe, it, expect, vi } from "vitest";

    const createResource = (id: number) => ({ id, data: `Resource ${id}` });
    const cleanupResource = vi.fn(); // 共享的 mock 函数

    // 如果测试间依赖或修改共享状态(如 cleanupResource),并发可能导致问题
    describe.concurrent("Potentially problematic concurrent tests", () => {
    it("test 1 modifies shared state", async () => {
    const resource = createResource(1);
    // 模拟操作
    await new Promise((r) => setTimeout(r, 50));
    cleanupResource(resource.id); // 调用共享 mock
    expect(cleanupResource).toHaveBeenCalledWith(1);
    });

    it("test 2 also modifies shared state", async () => {
    const resource = createResource(2);
    // 模拟操作
    await new Promise((r) => setTimeout(r, 30));
    cleanupResource(resource.id); // 并发调用可能导致调用次数或顺序混乱
    expect(cleanupResource).toHaveBeenCalledWith(2);
    // 如果 Test 1 的 expect 在此之后运行,可能会失败
    });
    });

    // 更安全的并发测试:确保测试间独立
    describe.concurrent("Safer concurrent tests", () => {
    const independentCleanup = vi.fn(); // 每个测试用独立的 mock (通过 setup/teardown)
    beforeEach(() => independentCleanup.mockClear());

    it("test A uses its own context", async () => {
    // ...
    independentCleanup("A");
    expect(independentCleanup).toHaveBeenCalledWith("A");
    });
    it("test B uses its own context", async () => {
    // ...
    independentCleanup("B");
    expect(independentCleanup).toHaveBeenCalledWith("B");
    });
    });
  • 配置 (示例) (vitest.config.ts):

    test: {
    // pool: 'threads', // 默认使用工作线程池
    // pool: 'forks', // 使用子进程池,隔离性更强,但开销更大
    poolOptions: {
    threads: {
    // minThreads: 1, // 最小线程数
    // maxThreads: 4, // 最大线程数 (默认 CPU 核数)
    // useAtomics: true, // 使用 Atomics 实现线程同步,可能更快但兼容性稍差
    },
    forks: {
    // minForks: 1,
    // maxForks: 4,
    }
    }
    }

2. 测试分组与分片(sharding)

  • 说明:

    • 分组 (describe): 主要是为了逻辑上组织相关的测试用例,提高可读性和可维护性。它本身不直接控制并行执行机制(除了可以对整个 describe 块应用 .concurrent)。
    • 分片 (Sharding): 是将整个测试套件分割成多个部分(片),然后在不同的机器或 CI 作业中并行运行这些部分。这是显著减少大型测试套件总执行时间的关键策略。Vitest 通过 --shard CLI 参数支持分片。
  • 示例代码 (Sharding):

    • 在 CI 环境中,假设你有 3 个并行的 CI 作业:

    • 作业 1: vitest run --shard='1/3' (运行测试套件的第 1 部分,共 3 部分)

    • 作业 2: vitest run --shard='2/3' (运行测试套件的第 2 部分,共 3 部分)

    • 作业 3: vitest run --shard='3/3' (运行测试套件的第 3 部分,共 3 部分)

    • Vitest 会根据文件路径或其他策略自动分配哪些测试文件属于哪个分片。

    • 示例 CI 配置 (GitHub Actions):

      jobs:
      test:
      runs-on: ubuntu-latest
      strategy:
      matrix:
      shard: [1, 2, 3] # 定义分片数量
      steps:
      # ... checkout, setup node, install deps ...
      - name: Run tests shard ${{ matrix.shard }} of 3
      run: pnpm test --shard='${{ matrix.shard }}/3' --coverage # 在每个作业中运行对应的分片
      # ... upload coverage etc. ...

3. 性能分析与优化建议

  • 说明: 随着测试套件增长,执行时间可能会变长。需要关注性能瓶颈并进行优化。
    • 识别慢测试: Vitest 默认会报告运行时间最长的测试(可在配置中调整数量 slowTestThreshold)。
    • 常见瓶颈:
      • 环境初始化: jsdomhappy-dom 的启动本身有开销。确保不需要 DOM 的测试运行在 node 环境。
      • 全局 setupFiles: 过于复杂的全局设置会拖慢每个测试文件的启动。
      • 组件渲染: 复杂组件的重复渲染可能很耗时。优化组件本身或使用更轻量级的测试策略(如只测逻辑,不渲染 UI)。
      • 低效 Mock: 过于复杂的 Mock 实现或不必要的深度 Mock。
      • 串行 I/O: 大量等待网络、文件系统等 I/O 操作的测试,如果相互独立,应考虑使用 .concurrent
    • 优化建议:
      • 选择合适环境: node > happy-dom > jsdom (速度和资源占用)。
      • 优化 setupFiles: 保持全局设置精简。
      • 使用 .concurrent: 对独立的 I/O 密集型测试启用并发。
      • 合理 Mock: 只 Mock 必要的部分,避免过度 Mock。
      • 利用缓存: Vitest (和 Vite) 有内置的转换缓存,确保配置正确以利用它。
      • 分片 (Sharding): 在 CI 中是最终的大杀器,通过增加硬件资源并行处理。
      • Node.js Profiling: 对于极端情况,可以使用 node --profnode --inspect 配合 Chrome DevTools 来分析 CPU 性能瓶颈。
  • 示例 (查看慢测试报告): Vitest 执行完毕后,控制台可能会显示类似信息:
    ✓ src/utils/complex.test.ts (3 tests) 5.8s
    ✓ should handle scenario A 120ms
    ✓ should handle scenario B 5500ms (slow) <-- 标记慢测试
    ✓ should handle scenario C 80ms

十一、CI/CD 与自动化

1. 与 GitHub Actions、GitLab CI 等集成

  • 说明: 将 Vitest 集成到 CI/CD 流程是标准实践,确保代码变更不会破坏现有功能。这通常涉及在 CI 配置文件中添加一个步骤来安装依赖并运行测试脚本。

  • 示例代码:

    • GitHub Actions (.github/workflows/ci.yml):

      name: CI

      on: [push, pull_request]

      jobs:
      build-and-test:
      runs-on: ubuntu-latest
      steps:
      - uses: actions/checkout@v3
      - name: Set up Node.js
      uses: actions/setup-node@v3
      with:
      node-version: "18"
      cache: "pnpm" # 或 'npm', 'yarn'

      - name: Install dependencies
      run: pnpm install --frozen-lockfile

      - name: Run linters # 可选步骤
      run: pnpm lint

      - name: Run tests
      run: pnpm test --run # 使用 'run' 模式,而不是 watch
      # 或者包含覆盖率: run: pnpm test --run --coverage
    • GitLab CI (.gitlab-ci.yml):

      image: node:18 # 使用包含 Node.js 的 Docker 镜像

      cache:
      key: ${CI_COMMIT_REF_SLUG}
      paths:
      - node_modules/
      - .npm/ # 如果使用 npm cache 或类似

      stages:
      - test

      test_job:
      stage: test
      before_script:
      - npm ci # 或 pnpm install / yarn install
      script:
      - npm test -- --run # 确保传递 'run' 参数给 vitest
      # 或 npm test -- --run --coverage
      artifacts: # 可选:保存覆盖率报告等
      when: always
      paths:
      - coverage/
      reports:
      junit: junit.xml # 如果配置了 JUnit 报告

2. 失败重试与稳定性

  • 说明: CI 环境有时会因为网络波动、资源竞争等原因导致测试偶然失败(Flaky Test)。Vitest 提供了自动重试失败测试的功能,以提高 CI 流程的稳定性。但过度依赖重试可能掩盖潜在的问题,应优先尝试修复 flaky 测试。
  • 配置:
    • CLI: vitest run --retry 3 (重试失败的测试最多 3 次)
    • 配置文件 (vitest.config.ts):
      test: {
      retry: 2, // 重试失败的测试 2 次
      }
  • 示例: 在 CI 脚本中直接使用 CLI 参数是常见的做法。
    # 在 CI 脚本中运行,带重试
    pnpm test --run --retry 2

3. 生成测试报告

  • 说明: 除了控制台输出,生成标准格式的测试报告(如 JUnit XML)对于 CI/CD 工具集成非常有用。许多 CI 平台可以解析这些报告,并在 UI 中展示详细的测试结果、历史趋势等。
  • 配置:
    • 配置文件 (vitest.config.ts):
      test: {
      reporters: [
      'default', // 默认控制台报告器
      'junit', // 添加 JUnit 报告器
      // 'json', // 添加 JSON 报告器
      // 'html' // 添加 HTML 报告器 (本地查看方便)
      ],
      outputFile: {
      junit: './junit.xml', // 指定 JUnit 报告输出路径
      // json: './test-results.json'
      // html: './test-report/index.html'
      },
      }
    • CLI (覆盖配置): vitest run --reporter=junit --outputFile=./junit.xml
  • 示例: 配置 JUnit 报告后,CI 平台(如 GitLab, Jenkins)通常可以自动拾取 junit.xml 文件并展示结果。

4. 代码质量与门禁

  • 说明: 测试和覆盖率是代码质量的重要保障。在 CI/CD 中,可以将测试结果和覆盖率作为“门禁” (Quality Gate),只有满足特定标准(例如,所有测试通过,代码覆盖率达到阈值)的构建才能继续进行(例如,部署到生产环境或允许合并代码)。
  • 实现:
    1. 测试通过: Vitest 在测试失败时会以非零状态码退出,这通常会自动使 CI 作业失败,阻止流程继续。
    2. 覆盖率阈值: 在 vitest.config.ts 中设置 coverage.thresholds。如果覆盖率低于阈值,Vitest 默认也会以非零状态码退出(检查 Vitest 文档确认当前版本的具体行为,可能需要配置 coverage.failUnderThresholds: true 或类似选项)。
  • 示例 (覆盖率门禁配置):
    // vitest.config.ts
    coverage: {
    // ... 其他覆盖率配置 ...
    thresholds: {
    lines: 85, // 要求行覆盖率达到 85%
    functions: 80,
    branches: 80,
    statements: 85,
    // 可以针对特定文件设置更高或更低的阈值
    // 'src/critical/**': { lines: 95 },
    },
    // 确保低于阈值时会失败 (检查 Vitest 版本文档,此选项可能已变更或不再需要)
    // failUnderThresholds: true,
    }

十二、最佳实践与进阶

1. 测试组织与目录结构

  • 说明: 合理的组织结构能提高测试的可维护性。常见模式:
    • Colocation (并置): 测试文件与源文件放在同一目录下,例如 src/components/Button.vuesrc/components/Button.test.ts。优点是查找方便,相关代码在一起。
    • Centralized (集中式): 所有测试文件放在一个顶层目录,如 tests/__tests__/,内部再按功能或模块组织子目录,例如 tests/components/Button.test.ts。优点是测试代码与业务代码分离清晰。
    • 混合模式: 根据项目规模和团队偏好选择。
  • 示例:
    • 并置:
      src/
      ├── components/
      │ ├── Button.vue
      │ └── Button.test.ts
      └── utils/
      ├── math.ts
      └── math.test.ts
    • 集中式:
      src/
      ├── components/
      │ └── Button.vue
      └── utils/
      └── math.ts
      tests/
      ├── components/
      │ └── Button.test.ts
      └── utils/
      └── math.test.ts

2. 公共测试工具与自定义 matcher

  • 说明:

    • 公共工具: 对于跨测试文件复用的设置逻辑(如创建 mock 服务器、生成特定数据)、复杂的断言逻辑或通用的辅助函数,可以抽取到公共的测试工具文件中(例如 tests/test-utils.ts)。
    • 自定义 Matcher: 当需要进行领域特定的断言,使得测试代码更具表达力时,可以扩展 Vitest 的 expect API,添加自定义匹配器 (Matcher)。
  • 示例代码:

    • 自定义 Matcher (setupTests.ts 或单独文件导入):

      // setupTests.ts
      import { expect } from "vitest";

      expect.extend({
      // 自定义一个 matcher 检查数字是否为正数
      toBePositive(received: number) {
      const pass = typeof received === "number" && received > 0;
      return {
      pass,
      message: () =>
      `expected ${received} ${pass ? "not " : ""}to be a positive number`,
      };
      },
      });

      // 在测试中使用
      // it('should be positive', () => {
      // expect(10).toBePositive();
      // expect(-5).not.toBePositive();
      // });

      // 需要为 TS 添加类型声明 (例如在 vitest.d.ts)
      // interface CustomMatchers<R = unknown> {
      // toBePositive(): R;
      // }
      // declare module 'vitest' {
      // interface Assertion<T = any> extends CustomMatchers<T> {}
      // interface AsymmetricMatchersContaining extends CustomMatchers {}
      // }
    • 公共测试工具 (tests/test-utils.ts):

      // tests/test-utils.ts
      export function createMockUser(overrides = {}) {
      return {
      id: Math.floor(Math.random() * 1000),
      name: "Test User",
      email: "test@example.com",
      isAdmin: false,
      ...overrides,
      };
      }

      // 在测试中使用
      // import { createMockUser } from '../test-utils';
      // it('should create an admin user', () => {
      // const adminUser = createMockUser({ isAdmin: true, id: 1 });
      // expect(adminUser.isAdmin).toBe(true);
      // expect(adminUser.id).toBe(1);
      // });

3. 测试数据管理与工厂

  • 说明: 测试往往需要构造各种输入数据或模拟状态。手动创建复杂对象很繁琐且容易出错。使用测试数据工厂 (Test Data Factories) 或库 (如 @faker-js/faker 生成模拟数据, factory-girl, fishery) 可以简化这个过程,生成一致或随机的测试数据。

  • 示例代码 (简单工厂函数): (参见上方 createMockUser 示例)

  • 示例代码 (使用 Faker.js):

    // tests/factories/userFactory.ts
    import { faker } from "@faker-js/faker"; // npm install -D @faker-js/faker

    export function buildUser(overrides = {}) {
    return {
    id: faker.string.uuid(),
    firstName: faker.person.firstName(),
    lastName: faker.person.lastName(),
    email: faker.internet.email(),
    avatar: faker.image.avatar(),
    isAdmin: false,
    createdAt: faker.date.past(),
    ...overrides,
    };
    }

    // 在测试中使用
    // import { buildUser } from '../factories/userFactory';
    // it('should generate realistic user data', () => {
    // const user1 = buildUser();
    // const user2 = buildUser({ isAdmin: true });
    // expect(user1.email).toContain('@');
    // expect(user2.isAdmin).toBe(true);
    // expect(user1.id).not.toBe(user2.id);
    // });

4. 大型项目测试策略

  • 说明: 在大型项目中,需要一个分层的测试策略,平衡测试覆盖率、执行速度和维护成本。
    • 测试金字塔:
      • 大量单元测试 (Unit Tests): (Vitest 强项) 测试独立的函数、模块或组件的逻辑,速度快,隔离性好。
      • 适量集成测试 (Integration Tests): (Vitest 适用) 测试多个单元如何协同工作,例如组件与其依赖的服务交互、API 路由与控制器等。可能需要 Mock 外部依赖。
      • 少量端到端测试 (E2E Tests): (使用 Playwright, Cypress 等) 测试整个应用程序的用户流程,在真实浏览器中运行,覆盖面广但速度慢、不稳定。
    • 策略要点:
      • 优先编写单元测试覆盖核心逻辑。
      • 集成测试验证关键交互点。
      • E2E 测试覆盖核心用户场景 (Happy Path)。
      • 利用 Vitest 的速度优势进行快速反馈。
      • 在 CI 中使用分片、缓存、选择性运行 (如 vitest related <changed_file>) 优化执行时间。
      • 定期审视测试覆盖率报告,识别未测试的关键区域。

5. 迁移 Jest/Jasmine/Mocha 测试到 Vitest

  • 说明: Vitest 设计时考虑了与 Jest API 的兼容性,迁移相对平滑。

    • 主要相似点: describe, it, expect, 钩子函数 (beforeEach 等) 的 API 非常相似。
    • 主要差异点/注意点:
      • Mocking: jest.fn() -> vi.fn(), jest.mock() -> vi.mock(), jest.spyOn() -> vi.spyOn(), jest.requireActual() -> vi.importActual(), jest.spyOn(object, method, 'get') -> vi.spyOn(object, method, 'get') (Getter/Setter spy 语法可能略有不同,需查阅文档)。
      • 全局变量: Jest 默认全局注入 API,Vitest 推荐配置 globals: false 并显式导入 (import { describe, it, expect, vi } from 'vitest'),但也可以设为 true 以兼容。
      • 配置: jest.config.js -> vitest.config.ts。配置项名称和结构不同,需要转换(例如 testEnvironment -> environment, setupFilesAfterEnv -> setupFiles)。利用 Vite 的配置是 Vitest 的优势。
      • ESM/CJS: Vitest 基于 Vite,原生支持 ESM。如果旧测试基于 CommonJS,可能需要处理模块导入/导出方式的差异,或配置 Vite/Vitest 处理 CJS 依赖。
      • 环境: Jest 的环境配置 (@jest/environment-jsdom) 需映射到 Vitest 的 environment (jsdom, happy-dom)。
      • 快照: 快照格式兼容,通常可以直接复用 .snap 文件。
    • 迁移工具: 社区可能有 codemods 或脚本辅助迁移,但手动检查和调整通常是必要的。
  • 示例 (语法对比):

    // Jest
    const mockFn = jest.fn();
    jest.mock("../src/utils");
    const utils = require("../src/utils");
    expect(mockFn).toHaveBeenCalled();

    // Vitest
    import { vi, expect } from "vitest";
    const mockFn = vi.fn();
    vi.mock("../src/utils"); // Hoisted
    // import * as utils from '../src/utils'; // Import after mock
    expect(mockFn).toHaveBeenCalled();

6. 常见问题与排查

  • 问题: document/window is not defined.
    • 原因: 测试运行在 node 环境,或未正确配置 jsdom/happy-dom 环境。
    • 排查: 检查 vitest.config.tstest.environment 配置。确保已安装 jsdomhappy-dom
  • 问题: Mock 不生效 (vi.mock 似乎没起作用)。
    • 原因: vi.mock 调用会被提升,确保 import 被模拟的模块vi.mock 之后。或者 vi.mock 的路径不正确。或者模块缓存问题。
    • 排查: 检查 import 顺序。确认路径。尝试 vi.resetModules()。考虑使用 vi.doMock 进行动态模拟。
  • 问题: 测试间状态泄漏 (一个测试影响了另一个)。
    • 原因: 在 beforeAll 或全局作用域修改了共享状态,或者测试内部没有正确清理副作用(如全局 mock、DOM 修改、定时器)。使用了 .concurrent 但测试逻辑不独立。
    • 排查: 使用 beforeEach/afterEach 进行设置和清理。使用 vi.clearAllMocks() / vi.restoreAllMocks()。避免在并发测试中修改共享状态。
  • 问题: 测试运行缓慢。
    • 原因: 见“性能分析与优化建议”部分。
    • 排查: 检查 Vitest 报告的慢测试。分析环境、设置、组件渲染、Mock 等。
  • 问题: 无法解析路径别名 (@/components)。
    • 原因: Vitest 配置中未正确设置 resolve.alias,或者与 vite.config.ts 中的别名不一致。
    • 排查: 确保 vitest.config.ts (或共享的 vite.config.ts) 中配置了正确的 resolve.alias
  • 问题: TypeScript 类型错误 / vi 全局变量未定义。
    • 原因: globals: true 未开启时需要显式导入 vi, describe 等。或者 TS 配置问题 (tsconfig.json)。
    • 排查: 推荐设置 globals: falseimport { ... } from 'vitest'。检查 tsconfig.json 是否正确配置(如 types: ["vitest/globals"] 如果使用全局变量)。

十三、生态与扩展

1. 插件系统

  • 说明: Vitest 深度集成 Vite,因此可以直接复用 Vite 的插件生态系统。大部分用于构建时转换、资源处理、注入变量等的 Vite 插件通常也能在 Vitest 环境下工作(例如 @vitejs/plugin-vue, @vitejs/plugin-react, 处理 CSS/图像的插件等)。这极大地简化了测试环境与开发/构建环境的配置一致性。Vitest 也有自己特定的插件钩子,用于更深度地定制测试行为,但这通常是为库作者或高级用例准备的。

  • 示例: 无需特殊配置,如果 vite.config.ts 中配置了插件,Vitest 运行时会自动加载它们。

    // vite.config.ts (会被 Vitest 读取)
    import { defineConfig } from "vite";
    import vue from "@vitejs/plugin-vue";
    import myCustomVitePlugin from "./plugins/my-vite-plugin";

    export default defineConfig({
    plugins: [
    vue(), // Vue 插件会在 Vitest 中处理 .vue 文件
    myCustomVitePlugin(), // 自定义 Vite 插件也可能在测试中生效
    ],
    test: {
    /* Vitest 配置 */
    },
    });

2. 与 Playwright/Cypress 等端到端测试工具协作

  • 说明: Vitest 主要聚焦于单元测试和集成测试,通常在模拟环境(Node.js 或 JSDOM/HappyDOM)中快速运行。而 Playwright、Cypress 等是端到端 (E2E) 测试工具,它们在真实浏览器中运行,模拟用户与整个应用程序的交互流程。它们与 Vitest 是互补关系,共同构成分层的测试策略:
    • Vitest: 测试代码逻辑、组件单元、模块集成,速度快,反馈及时。
    • Playwright/Cypress: 测试关键用户流程、跨页面导航、真实浏览器兼容性、视觉回归,速度相对慢,但更接近真实用户体验。
  • 协作方式:
    • 两者通常在不同的测试套件和运行命令下执行。
    • 可以在同一个项目中维护,例如 pnpm test:unit (运行 Vitest) 和 pnpm test:e2e (运行 Playwright/Cypress)。
    • CI/CD 流程中可以包含这两个阶段。