[手写 Umi]为 konos-core 集成 vitest

3,382 阅读3分钟

之前几个文章,我们的测试方式都是通过人工运行 version 命令,看看控制台是否正确打印版本号,来实现的。这种方式在我们的项目越来越复杂之后,就会变得越来越不可靠。因此引入测试方案,来保证我们的每一次修改都不会造成意外的破坏。

技术选型

vitest

先说结论,我们将使用 vitest 来运行我们的单元测试用例,最大的原因是配置简单并且运行速度快。

关于什么是 vitest,测试用例怎么写,怎么测试 React 组件等内容,后续我再写文章说明。

搭建 vitest 环境

相比起 jest 需要的做的事情,搭建 vitest 环境真的是相当的无脑且轻松(也可能是我没用 jest 搞过这么简单的例子)。

pnpm i vite vitest

修改 package.json

  "scripts": {
    "build": "father build",
    "dev": "father dev",
    "version": "node dist/cli version",
    "test": "vitest run",
    "test:watch": "vitest"
  },

编写测试用例

新建文件 src/service.test.ts,编写如下测试用例:

import { describe, it, expect } from "vitest";
import { Service } from "./service";
import { join } from "path";

describe("Service", async () => {
  it("run version", async () => {
    const pkg = require(join(__dirname, "..", "package.json"));
    const version = await new Service({
      plugins: [
        join(__dirname, "onstart.ts"),
        join(__dirname, "onend.ts"),
        join(__dirname, "version.ts"),
      ],
    }).run({ name: "version" });
    expect(version).toBe(pkg?.version);
  });
});

简单的分析上面的代码,我们新建了一个服务,它加载了我们之前编写的三个插件。然后我们用这个服务运行了 nameversion 的命令。这个和我们之前做的测试是对应的(执行 node dist/cli version),然后我们断言结果为 package.json 中写明的 version。这也是我们之前的行为,通过控制台确认打印的版本号是否正确。

通过类似的行为比对,我们可以先明确这一点,我们写测试用例的目的,就是为了来验证我们的代码行为。

有一个这样的过程:

1编写代码 -> 2编写测试 -> 2.1 通过 -> 完成
               v
1编写代码 <- 2.2 失败

其实相比起上诉过程,我觉得如下的过程,会更直观一些

1编写测试 -> 2编写代码 -> 2.1 通过 -> 完成
             v    ^
            2.2 失败

因为在做某件事情之前,相比起代码,往往我们更清楚我们要的“表现”(结果),比如某个函数它入参和出参,某个页面最终展现等。

这也是去年开始很火的 DDD,测试驱动开发。关于这部分的内容,后续我再写文章说明。

执行测试用例

执行 pnpm test,你将会看到一个错误(Unexpected token 'export'),这是因为我们在加载插件文件的时候使用了 require - ret = require(plugin);,在我们手动测试的时候(node dist/cli version)我们执行的时候 dist 下的文件,这时文件已经通过 build (father build)转译过了。

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ Failed Tests 1 ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯

 FAIL  src/service.test.ts > Service > run version
Error: 插件 /Users/congxiaochen/Documents/konos-core/src/onstart.ts 获取失败,可能是文件路径错误,详情日志为 Unexpected token 'export'
 ❯ Service.getPlugin src/service.ts:38:13
     36|       ret = require(plugin);
     37|     } catch (e: any) {
     38|       throw new Error(
       |             ^
     39|         `插件 ${plugin} 获取失败,可能是文…
     40|       );
 ❯ Service.initPlugin src/service.ts:47:28
 ❯ Service.run src/service.ts:75:18
 ❯ src/service.test.ts:14:8

Module Error: 插件 /Users/congxiaochen/Documents/konos-core/src/onstart.ts 获取失败,可能是文件路径错误,详情日志为 Unexpected token 'export' seems to be an ES Module but shipped in a CommonJS package. You might want to create an issue to the package "Error: 插件 " asking them to ship the file in .mjs extension or add "type": "module" in their package.json.

使用 require 加载 ts 文件

修改 getPlugin 函数

我们在 getPlugin 函数中使用 @umijs/utils 中的 register 工具

- import { yParser } from "@umijs/utils";
+ import { yParser, register } from "@umijs/utils";
+ import esbuild from "esbuild";

...

 async getPlugin(plugin: string) {
    let ret;
    try {
      register.register({
        implementor: esbuild,
        exts: [".ts"],
      });
      register.clearFiles();
      ret = require(plugin);
    } catch (e: any) {
      throw new Error(
        `插件 ${plugin} 获取失败,可能是文件路径错误,详情日志为 ${e.message}`
      );
    } finally {
      register.restore();
    }
    return ret.__esModule ? ret.default : ret;
  }

主要功能就是当 require 文件后缀为 .ts 时,使用 esbuild 对文件进行转移。

如何实现

主要是给 request 添加一个 hook ,这能力来自 [pirates](https://github.com/danez/pirates)

import { addHook } from "pirates";

addHook(
  (code, filename) => {
    files.push(filename);
    const ext = extname(filename);
    try {
      return esbuild.transformSync(code, {
        sourcefile: filename,
        loader: ext.slice(1),
        target: "es2019",
        format: "cjs",
        logLevel: "error",
      }).code;
    } catch (e) {
      throw new Error(`Parse file failed: [${filename}]`, { cause: e });
    }
  },
  {
    ext: opts.exts || HOOK_EXTS,
    ignoreNodeModules: true,
  }
);

再次运行测试 pnpm test:

konos-core git:(main) ✗ pnpm test

> konos-core@1.0.0 test /konos-core
> vitest run

stdout | src/service.test.ts > Service > run version
开始:执行了 version 插件
konos@1.0.0
结束:执行了 version 插件

 ✓ src/service.test.ts (1) 425ms

 Test Files  1 passed (1)
      Tests  1 passed (1)
   Start at  18:44:39
   Duration  1.12s (transform 71ms, setup 0ms, collect 427ms, tests 425ms)

本次代码变更记录

源码归档