👋 一起写一个基于虚拟模块的密钥管理 Rollup 插件吧(四)

上一章 我们成功将插件迁移到 Unplugin 插件系统,使其同时支持 Vite、Rollup、Webpack、Esbuild 等多种构建工具,让更多用户都能轻松体验到我们基于虚拟模块的密钥管理方案。

然而,尽管我们的插件功能已经完整实现,但是在未来的迭代过程中仍然存在潜在风险。插件可能因为版本更新、构建工具差异或者代码修改而出现功能回归、虚拟模块解析异常或类型声明生成不正确等问题。

为了确保插件在各种环境下始终稳定可靠,本章我们将会为插件编写单元测试,及时发现和防止潜在问题,从而为插件的持续维护和升级提供安全保障!

框架选型

我们的插件设计之初便考虑为 Vite 提供优先支持,所以对于单元测试框架自然第一时间想到的就是 Vitest,那么 Vitest 有哪些优势呢?

  • 与 Vite 通用的配置、转换器、解析器和插件。
  • 智能文件监听模式,就像是测试的 HMR!
  • 支持对 Vue、React、Svelte、Lit 等框架进行组件测试。
  • 开箱即用的 TypeScript / JSX 支持。
  • 支持套件和测试的过滤、超时、并发配置。
  • ...

Jest

Jest 在测试框架领域占据了主导地位,因为它为大多数 JavaScript 项目提供开箱即用的支持,具备舒适的 API(it 和 expect),且覆盖了大多数测试的需求(例如快照、模拟和覆盖率)。

在 Vite 项目中使用 Jest 是可能的,但是在 Vite 已为最常见的 Web 工具提供了支持的情况下,引入 Jest 会增添不必要的复杂性。如果你的应用由 Vite 驱动,那么配置和维护两个不同的管道是不合理的。如果使用 Vitest,你可以在同一个管道中进行开发、构建和测试环境的配置。

Cypress

Cypress 是基于浏览器的测试工具,这对 Vitest 形成了补充。如果你想使用 Cypress,建议将 Vitest 用于测试项目中不依赖于浏览器的部分,而将 Cypress 用于测试依赖浏览器的部分。

Cypress 的测试更加专注于确定元素是否可见、是否可以访问和交互,而 Vitest 专注于为非浏览器逻辑提供最佳的、快速的开发体验。

单元测试

在编写插件或工具库时,单元测试主要用于验证每个独立功能模块的行为是否正确,它通常具有以下特点:

  1. 细粒度:测试目标是最小的可测试单元(函数、方法、类);
  2. 隔离性:各测试相互独立,不依赖执行顺序或外部环境;
  3. 可重复:相同的输入应产生相同的输出,便于回归测试;
  4. 快速执行:测试运行速度快,适合频繁执行;
  5. 自动化:通常集成到构建或持续集成(CI)流程中。

快速上手

首先使用 npm 将 Vitest 安装到项目:

shell 复制代码
# pnpm
pnpm add -D vitest

# yarn
yarn add -D vitest

# npm
npm install -D vitest

然后可以编写一个简单的测试来验证将两个数字相加的函数的输出:

typescript 复制代码
// sum.ts

export function sum(a: number, b: number) {
  return a + b;
}
typescript 复制代码
// sum.test.ts

import { expect, it } from "vitest";
import { sum } from "./sum";

it("adds 1 + 2 to equal 3", () => {
  expect(sum(1, 2)).toBe(3);
});

一般情况下,执行测试的文件名中必须包含 .test..spec.

接下来,为了执行测试,将以下部分添加到 package.json 文件中:

json5 复制代码
// package.json

{
  "scripts": {
    "test": "vitest"
  }
}

最后,运行 npm run testyarn testpnpm test,具体取决于你的包管理器,Vitest 将打印此消息:

shell 复制代码
✓ sum.test.ts (1)
  ✓ adds 1 + 2 to equal 3

Test Files  1 passed (1)
     Tests  1 passed (1)
  Start at  02:15:44
  Duration  311ms

我们轻松入门了使用 Vitest 编写单元测试!

开始编码

接下来我们为插件的各个模块编写单元测试,测试文件放在 test 目录中,使用 .test.ts 后缀命名。

crypto-splitter

typescript 复制代码
// crypto-splitter.test.ts

import { describe, expect, it } from "vitest";
import { combine, split } from "../packages/crypto-splitter/src";

describe("crypto-splitter", () => {
  it("returns empty array for empty string", () => {
    expect(split("")).toEqual([]);
  });

  it("returns empty string for empty chunks", () => {
    expect(combine([])).toBe("");
  });

  it("splits into default 4 segments and combines correctly", () => {
    const key = "iamxiaohe";

    const chunks = split(key);
    expect(chunks).toHaveLength(4);

    expect(combine(chunks)).toBe(key);
  });

  it("splits into custom number of segments and combines correctly", () => {
    const key = "iamxiaohe";

    const chunks = split(key, { segments: 6 });
    expect(chunks).toHaveLength(6);

    expect(combine(chunks)).toBe(key);
  });

  it("different splits produce different chunks but combine correctly", () => {
    const key = "iamxiaohe";

    const chunks1 = split(key);
    const chunks2 = split(key);

    expect(chunks1).not.toEqual(chunks2);

    expect(combine(chunks1)).toBe(key);
    expect(combine(chunks2)).toBe(key);
  });
});
  1. 空字符串 → 应返回空数组;
  2. 空数组 → 应还原为空字符串;
  3. 默认会拆成 4 段,并能正确合并;
  4. 可自定义段数(比如 6 段),也能正确合并;
  5. 同一个字符串多次拆分结果不同(说明有随机性),但都能还原原文。

getCode

typescript 复制代码
// code.test.ts

import { unlink, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { describe, expect, it } from "vitest";
import { getCode } from "../packages/shared/src";

describe("getCode", () => {
  it("should generate code that exports correct key values", async () => {
    const keys = {
      key1: "iamxiaohe",
      key2: "ilovexiaohe"
    };

    const temp = join(__dirname, "virtual-code.js");

    await writeFile(temp, getCode(keys));

    const { key1, key2 } = await import(temp);

    expect(key1).toBe(keys.key1);
    expect(key2).toBe(keys.key2);

    await unlink(temp);
  });
});

先准备一个包含若干键值的对象 keys,调用 getCode(keys) 得到生成的代码字符串,然后将其写入临时文件 virtual-code.js。通过动态 import 方式加载这个文件,检查其中导出的变量 key1key2 是否与原始对象中的值完全一致,最后删除临时文件。

writeDeclaration

typescript 复制代码
// declaration.test.ts

import { ensureFile, outputFile } from "fs-extra";
import { describe, expect, it, vi } from "vitest";
import { writeDeclaration } from "../packages/shared/src";

vi.mock("fs-extra", () => ({
  ensureFile: vi.fn(),
  outputFile: vi.fn()
}));

describe("writeDeclaration", () => {
  it("should create a declaration file with default name when dts is true", async () => {
    await writeDeclaration(
      {
        key1: "iamxiaohe",
        key2: "ilovexiaohe"
      },
      {
        moduleId: "virtual:crypto-key",
        dts: true
      }
    );

    expect(ensureFile).toHaveBeenCalledWith("crypto-key.d.ts");
    expect(outputFile).toHaveBeenCalledWith(
      "crypto-key.d.ts",
      `declare module "virtual:crypto-key" {
  export const key1: string;
  export const key2: string;
}`
    );
  });

  it("should create a declaration file with custom path when dts is a string", async () => {
    await writeDeclaration(
      {
        key1: "iamxiaohe"
      },
      {
        moduleId: "virtual:crypto-key",
        dts: "types/crypto-key.d.ts"
      }
    );

    expect(ensureFile).toHaveBeenCalledWith("types/crypto-key.d.ts");
    expect(outputFile).toHaveBeenCalledWith(
      "types/crypto-key.d.ts",
      `declare module "virtual:crypto-key" {
  export const key1: string;
}`
    );
  });
});
  1. 模拟文件操作 :通过 vi.mock("fs-extra") 模拟 ensureFileoutputFile,避免实际读写磁盘。
  2. 测试默认路径 :当 dts: true 时,writeDeclaration() 应生成默认文件名 crypto-key.d.ts,并写入对应的模块声明和键值类型。
  3. 测试自定义路径 :当 dts 是字符串(自定义路径)时,应生成指定路径的声明文件,并写入正确内容。
  4. 验证调用 :通过 expect(...).toHaveBeenCalledWith(...) 检查 ensureFileoutputFile 是否被正确调用,确保文件路径和内容符合预期。

运行测试与结果

Vitest 通过 v8 支持原生代码覆盖率,通过 istanbul 支持检测代码覆盖率。

这里我们选择 Vitest 默认的 v8 作为覆盖工具,在 vitest.config.ts 中配置 providerv8 并指定 include 配置覆盖率报告中需要统计的文件范围:

typescript 复制代码
// vitest.config.ts

import { defineConfig } from "vitest/config";

export default defineConfig({
  test: {
    coverage: {
      provider: "v8",
      include: [
        "packages/*/src/**/*.ts"
      ]
    }
  }
});

然后在 package.json 中添加 coverage 配置:

json5 复制代码
// package.json

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

现在执行 test:coverage 就可以运行测试并且输出单元测试覆盖率啦!

shell 复制代码
Coverage enabled with v8

 ✓ test/crypto-splitter.test.ts (5 tests) 2ms
 ✓ test/declaration.test.ts (2 tests) 2ms
 ✓ test/code.test.ts (1 test) 5ms

 Test Files  3 passed (3)
      Tests  8 passed (8)
   Start at  13:54:48
   Duration  279ms (transform 61ms, setup 0ms, collect 96ms, tests 9ms, environment 0ms, prepare 176ms)

 % Coverage report from v8

---------------------|---------|----------|---------|---------
File                 | % Stmts | % Branch | % Funcs | % Lines 
---------------------|---------|----------|---------|---------
All files            |     100 |      100 |     100 |     100 
 crypto-splitter/src |     100 |      100 |     100 |     100 
  combine.ts         |     100 |      100 |     100 |     100 
  split.ts           |     100 |      100 |     100 |     100 
 shared/src          |     100 |      100 |     100 |     100 
  code.ts            |     100 |      100 |     100 |     100 
  declaration.ts     |     100 |      100 |     100 |     100 
---------------------|---------|----------|---------|---------

🎉 所有测试用例全部通过,并且测试覆盖率达到 100%!

这意味着插件的核心逻辑已全部经过验证,不仅功能正确,而且具备极高的稳定性与可维护性。

源码

插件的完整代码可以在 virtual-crypto-key 仓库中查看。赠人玫瑰,手留余香,如果对你有帮助可以给我一个 ⭐️ 鼓励,这将是我继续前进的动力,谢谢大家 🙏!

总结与回顾

至此,我们已经为插件建立了完善的单元测试体系,使用 Vitest 对各个核心模块进行了自动化验证,确保:

  • 🔐 密钥拆分与还原逻辑正确无误
  • 🧩 生成虚拟模块代码行为符合预期
  • 🧾 类型声明文件生成逻辑正确
  • ✅ 整体代码质量和覆盖率达标

回顾整个系列,我们从需求分析、插件设计、虚拟模块实现,到 TypeScript 支持、多构建工具迁移,再到如今的测试验证,完整经历了一个现代化插件从无到有的开发全流程。

如果你一路读到了这里,那说明你已经具备独立开发一个可发布插件的能力,不仅了解了 Rollup / Vite 插件机制的底层逻辑,也掌握了 Unplugin 的跨构建工具开发模式和 Vitest 的测试方法。

未来,你完全可以基于本系列的思路继续扩展更多特性,比如:

  • 支持更复杂的密钥混淆算法
  • 添加 CI 流程自动化测试
  • 发布到 npm 供更多开发者使用

祝贺你完成了这场关于插件设计、类型系统与测试驱动开发的完整旅程!

本系列到此完结,感谢你的阅读与坚持,我是 xiaohe0601,我们下一个项目再见!👋

相关推荐
parade岁月8 小时前
nuxt和vite使用环境比变量对比
前端·vite·nuxt.js
一枚前端小能手11 小时前
「周更第7期」实用JS库推荐:Vite
前端·javascript·vite
萌萌哒草头将军1 天前
尤雨溪强烈推荐的这个库你一定要知道 ⚡️⚡️⚡️
前端·vue.js·vite
chenjianzhong1 天前
vite-plugin-legacy 实战解析
前端·vue.js·vite
小小前端_我自坚强3 天前
2025Webpack、Vite、Rollup详解
webpack·vite·rollup.js
静待雨落5 天前
vite如何配置https
vite
井柏然6 天前
为什么打 npm 包时要将 Vue/React 进行 external 处理?
javascript·vite·前端工程化
千码君20166 天前
React Native:快速熟悉react 语法和企业级开发
javascript·react native·react.js·vite·hook
星光不问赶路人8 天前
一文搞清楚 TypeScript 中 triple-slash 与 tsconfig.types 有何区别?
typescript·vite