之前几个文章,我们的测试方式都是通过人工运行 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);
});
});
简单的分析上面的代码,我们新建了一个服务,它加载了我们之前编写的三个插件。然后我们用这个服务运行了 name 为 version 的命令。这个和我们之前做的测试是对应的(执行 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)