增加TS测试
增加js测试和增加ts测试没有本质区别,在测试框架用法上是一样的。唯一增加的就是ts引入类型,所以需要额外做类型测试。
使用Node.js内置测试框架
Node.js诞生自2009年,在v18之前的13年时间里都没有内置任何测试框架。一直都是使用npm生态。像狼书系列卷三中提到的几个测试框架,都已经有5年以上的历史了。
测试框架 | 当前主要版本 | 年限 |
---|---|---|
mocha | v10 | 11 |
tap | v16 | 11 |
tape | v5 | 10 |
ava | v5 | 9 |
jest | v27 | 7 |
Node.js遵循与JavaScript本身相同的"最小核心"原则。因此,像代码检查工具、代码格式化工具和测试运行器这样的工具最好作为第三方工具提供。虽然这是一个很好的想法很长一段时间,但现在没有标准测试工具的任何语言都显得有些奇怪。Deno、Rust和Go - 它们都有自己内置的测试运行器。
目前各个测试框架和Node.js内置测试框架差异对比如下,参考自https://glebbahmutov.com/blog/trying-node-test-runner/。
特性 | Mocha | Ava | Jest | Node.js TR | 推荐 |
---|---|---|---|---|---|
内置在Node中 | 🚫 | 🚫 | 🚫 | ✅ | 🎉 |
Watch 模式 | ✅ | ✅ | ✅ | ✅ | 🎉 |
Reporters | lots | via TAP | lots | via TAP | |
Assertions | via Chai ✅ | ✅ | ✅ | weak | 😑 |
Snapshots | 🚫 | ✅ | ✅ | 🚫 | |
Hooks | ✅ | ✅ | ✅ | ✅ | |
grep support | ✅ | ✅ | ✅ | ✅ | |
spy and stub | via Sinon ✅ | via Sinon ✅ | ✅✅ | ✅ | |
parallel execution | ✅ | ✅ | ✅ | ✅ | |
code coverage | via nyc | via c8 | ✅ | ✅ | 👍 |
TS support | via ts-node | via ts-node | via ts-jest | via ts-node | 🐢 |
在Node.js v18开始内置了测试框架,在Node.js v20版本中,已经被标记为Stable能力,大家可以放心使用。
import {test, describe} from 'node:test';
import assert from 'node:assert';
import { HelloWorld } from "./helloworld.mjs";
describe("test suite", function() {
test("test if works correctly", function() {
// run some test
assert.strictEqual(1, 1);
})
})
此时,执行npm test或node —test —watch就可以了。
$ npm test
> node20@1.0.0 test
> node --watch --test .
▶ test suite
✔ test if works correctly (0.104958ms)
▶ test suite (0.894917ms)
使用Tsd类型测试
写了类型,最好写好类型测试,在ts生态里,可以使用tsd来做类型测试。
创建index.test-d.ts文件
import { expectType } from "tsd";
import { IPerson } from ".";
import { HelloWorld } from "./src/helloworld";
const cli: IPerson = new HelloWorld();
expectType<Promise<void>>(cli.sayHi("use TypeScript to write Node.js"));
执行
$ npx tsd
如果没有任何显示,说明测试通过。如果有错误就会有下面这样的显示。
再思考一下
- 有代码,就要测试
- 有类型,就要有类型测试
如果想规范的写,就会比较麻烦一点。如果什么都不写,也没啥问题,只是不标准而已。像Rails这种追求极致效率,又想标准的项目,不用ts是正常的。
测试规范
在测试领域中,规范(specification, spec) 包含了用例的描述以及针对它们的测试,如下所示:
import { it, describe } from "node:test";
import assert from "node:assert";
import { HelloWorld } from "../src/helloworld";
import { IPerson } from "..";
describe("test suite", function () {
it("test if works correctly", async function (t) {
const log = t.mock.method(global.console, "log");
assert.strictEqual(log.mock.callCount(), 0);
// call hello world say method
const cli: IPerson = new HelloWorld();
await cli.sayHi("liangqi");
assert.strictEqual(log.mock.callCount(), 1);
});
it("test if works incorrectly", async function () {
const cli: IPerson = new HelloWorld();
assert.rejects(async () => await cli.sayHi(), new Error("fail"));
});
});
正如你所看到的,一个规范包含三个主要的模块:
1、describe("title", function() { ... })
表示我们正在描述的功能是什么,相当于一个group。用于组织“工人(workers)” —— it
代码块。
2、it("use case description", function() { ... })
it
里面的描述部分,我们以一种易于理解 的方式描述特定的用例,第二个参数是用于对其进行测试的函数。从Node.js v18开始,it()
被引入并且被正式定义为test()
的别名,因此两者提供的功能完全相同。我们在describe
中使用it
有助于增加代码的可读性,表示这是"一系列测试"中的一项,相当于item,如何测试?测试逻辑?都是在it的回调函数中实现的。
3、assert.equal(value1, value2)
it
块中的代码,如果实现是正确的,它应该在执行的时候不产生任何错误。
assert.*
函数用于检查 测试 函数是否按照预期工作。在这里我们使用了其中之一 —— assert.equal
,它会对参数进行比较,如果它们不相等则会抛出一个错误。这里它检查了 pow(2, 3)
的值是否等于 8
。还有其他类型的比较和检查,我们将在后面介绍到。
规范可以被执行,它将运行在 it
块中指定的测试。我们稍后会看到。
开发流程通常看起来像这样:
- 编写初始规范,测试最基本的功能。
- 创建一个最初始的实现。
- 检查它是否工作,我们运行测试框架(很快会有更多细节)来运行测试。当功能未完成时,将显示错误。我们持续修正直到一切都能工作。
- 现在我们有一个带有测试的能工作的初步实现。
- 我们增加更多的用例到规范中,或许目前的程序实现还不支持。无法通过测试。
- 回到第 3 步,更新程序直到测试不会抛出错误。
- 重复第 3 步到第 6 步,直到功能完善。
如此来看,开发就是不断地 迭代。我们写规范,实现它,确保测试通过,然后写更多的测试,确保它们工作等等。最后,我们有了一个能工作的实现和针对它的测试。
让我们在我们的开发案例中看看这个开发流程吧。
在我们的案例中,第一步已经完成了:我们有一个针对 helloworld 的初始规范。因此让我们来实现它吧。但在此之前,让我们用一些 JavaScript 库来运行测试,就是看看测试是通过了还是失败了。