概述
本文是笔者的系列博文 《Bun技术评估》 中的第十篇。
本文主要探讨的内容,评估和理解,bun是如何支持和实现开发工作的一个非常重要的环节,就是Testing(测试)的。和很多其他的模块类似,bun也内置了test支持相关的模块和功能,可以直接使用。
关于这方面的内容,主要参考了bun官方技术文档的相关章节,其链接如下:
关于测试
首先需要说明,笔者的主要工作并非测试相关,原来对测试技术也不甚精通,包括在自己的日常开发过程中,相关的理解和应用也不是很多。但之所以考虑在本系列文章中,规划和增加了讨论测试相关的内容,主要是由于看到bun本身集成内置了一个测试框架,可以更加方便和集成的支持开发应用,觉得这方面的内容也非常实用和重要。并且经过一段时间的测试、使用和理解,笔者感觉,bun提供的测试框架还是非常不错的。整体风格还是简单直接,方便快捷,可以帮助开发者快速验证一些设计和构想。是比较值得在日常开发中,比较日常化的,有意识的融入开发流程的。
笔者感觉,在开发过程和开发环境中,从开发支持的维度,集成测试相关的流程和功能,有很多好处。bun testing框架,也提供了很大方便的功能特性,来实现这种开发和测试集成的模式。
- 单元测试
由于测试框架和开发框架完全是集成和一体的,它们共享同一种语义,同一个环境配置,同一套依赖库,所以使用bun testing进行单元测试也是非常简单方便的。就跟在项目中,运行一套和应用软件相同的平行的验证程序一样。要来验软件功能和模块,这就是对单元测试的无缝支持。
- 集成测试
现代化的Web应用程序,都有相当程度的复杂性。就是它一般不仅仅是一个封闭系统,而是需要和其他的系统进行集成和交互。典型的情况就是,Web应用一般需要数据库系统进行集成,来进行数据的处理和交互。这种情况下的功能模块就比单元内部的情况复杂一些。当然这对于集成在开发环境中的测试模块,是没有什么太大的影响的,因为它们调用和执行的方式,就是程序本身执行的方式。
- 端到端测试(系统测试)
由于内置了非常丰富的功能模块,使用bun testing实现端到端的测试,也是比较方便的。例如在测试代码中,可以直接在测试时启动一个API服务器,然后使用内置的http client去模拟应用场景通过网络来访问和操作API,甚至可以编写和模拟一个多次请求响应的业务流程,从而实现最贴近实际实用场景的操作过程。而这些内容和控制,都可以在一个bun testing用例中实现。
- 数据操作
实际上,笔者使用bun testing测试框架,不仅实现了API的测试,还实现了模拟的数据管理任务,因为这个测试框架,提供了很大的灵活性,可以几乎完全的模拟人类在Web应用过程中的数据操作。
在理解了为什么bun testing可以很好的支撑日常开发和数据管理工作之后,下面我们就可以来实际探讨一下,bun testing是如何实现和操作的。
基本形式
我们先来看一看一个最简单的测试用例文件的编写和执行,感受一下bun测试操作的简洁和方便。
一般情况下,我们使用一个test文件夹,来存储编写的测试文件。测试文件的命名有一定的规范,通常以test.ts作为结尾,前面的文件名,可以按照业务需求来取。如果有多个测试内容,应当按照业务需求,分功能和模块编写多个测试文件。如果有需求,也可以通过文件命名,来控制测试文件执行的顺序。
测试文件的具体内容如下:
test/t1.test.ts
describe("First Unit Test", () => {
it("1+1", ()=>{
let r = 2;
expect(1+1).toBe(r);
} );
});
这是一个几乎最简单的测试程序结构。
首先是一个测试项目,执行名为describe的方法。它有两个参数,包括一个标题,来描述当前测试项目;和测试的操作即一个执行函数。执行函数内部包括一系列测试方法(it),用于测试时调用和执行。示例中测试项目也只有一个测试方法,名为"1+1",它也有对应的执行函数,就是实际测试执行的代码了。测试方法中一般包括一个或者多个断言(expect),用于检查测试代码的执行结果是否符合预期,作为测试项目是否通过的标准。
可能有读者有疑问,就是这些方法好像都没有预先的定义、引用和声明,怎么可以执行呢? 笔者理解,如果使用bun test来执行测试文件,这些方法都会被自动引用,无需引用和声明,非常直接方便。
测试用例编写好之后,就可以直接在项目文件夹中,执行bun test命令,即可执行测试,并且可以查看测试结果和报告。
当然,这只是最基础的结构和使用,我们先建立一个基本的概念,关于测试文件更多的信息和测试报告的解读,后面会详细展开讨论。
编写测试文件
按照bun test技术文档的说法,bun的测试执行框架和实现,遵循jest的规范,所以熟悉jest的开发者应当感到非常熟悉和方便。但笔者以前对于测试框架和操作,接触的不是很多,所以有一些东西需要熟悉和了解。
- 测试文件名称
bun test可以自动处理以下形式的测试文件名称:
{.test| _test|.spec|_spec}.{js|jsx|ts|tsx}
当然,在测试时,也可以指定执行特定文件名或者文件夹下的测试文件。
- test/it 测试
在基本形式章节中,我们看到的基本测试方法是it,但在技术文档中,都使用test,两者其实是一样的。it这个名称,更加人性化,意思是"它是..."。
在这个方法中,开发中或测试人员,就可以实际的编写各个层级测试的实现代码。所有的测试项目,都可以抽象成为准备测试数据-调用要测试的方法-检查执行结果这一标准化的测试流程。然后还可以将这些流程例举或者组织起来,就可以逐步完善和完成整个应用程序的测试。
- descript 描述
笔者理解,descript是一种基本测试方法的分组和组织形式(名字空间),使测试方法的编写、管理和测试结果的解读更加方便。它可能本身并没有太大的意义。但可以帮助进行组织测试项目,特别是建立测试项目前后的依赖关系,和管理测试工作的生命周期。
- expect 期望
测试工作的结果,需要通过一系列的结果和执行预期的检查(也称为断言 asser标示来表现。如果符合预期,就表示当前测试项目是通过的,否则就认为程序实现有问题。这些在bun testing框架中,是通过一系列espect调用来实现的。这部分内容比较多,我们后面有专门针的章节详细展开说明。
测试执行和报告
测试执行后,我们通常会得到类似以下内容的执行结果和报告:
shell
bun test
bun test v1.2.14 (6a363a38)
test\t1.test.ts:
✓ First Unit Test > 1+1 [16.00ms]
1 pass
0 fail
1 expect() calls
Ran 1 tests across 1 files. [303.00ms]
这个结果其实中包含了比较丰富的内容可以供我们解读。包括:
- 执行了哪些测试文件
- 测试文件中,测试方法通过或者失败的数量
- 测试方法中,断言调用的数量
- 每一个测试文件执行所花费的时间,和总测试时间,可以简单的用于估计程序执行的性能
- 每个测试,可以看到其归属的测试项目(descript)和子项目(it)
断言
jest中使用断言来作为测试结果和是否通过测试的检查方式,其标准形式为:
expect(somevalue).matcher(othervalue)
这里:
- expect是一个固定的方法
- mathcher是一个可选的检查方法
- 它的意思是,期望(这个值)某种匹配(另外一个值)
场景的mather包括:
- not: 可以链接和修饰断言方法
- toBe: 是某个值,通常用于检查数组类型是否符合预期
- toEqual/toStrictEqual: 检查两个值相等,通常用于检查字符串或者对象内容是否等值
- toBeNull/toBeUndefined/toBeNan: 各种空的判断
- toContain/toContainValue...: 判断集合的包含性
- toBeGreaterThan/toBeLessThan...: 大小检查
- toBeCloseTo: 经常用于浮点数的断言,因为数组计算处理的方式,和通常的人类认证有差异
- toMatch: 字符串检查,包括正则
- toThrow: 检查调用时是否抛出错误,这是expect的参数,应当是函数调用
笔者觉得这个jest的断言检查方法集,有点设计过度了,本质上就是检查某种条件是真还是假,js已经提供了非常强大的对象操作和简洁的真值检查方式。足以应对绝大多数判断的场景。
测试生命周期
可以为测试文件(file)和测试项目(descript),设置执行生命周期的hook(钩子)。测试的生命周期,值得是,除了测试工作本身之外,为了保证测试工作的顺利执行和符合测试的要求,还可能需要一些额外设置和组织工作。如这些钩子方法包括:
- beforeAll: 在所有测试开始前执行一次,通常用于设置环境
- beforeEach: 在每个测试项目先执行,按需要执行
- afterEach: 在每个测试项目后执行,按需要执行
- afterAll: 在所有测试项目后执行,通常用于执行清除和扫尾工作,如恢复数据和环境
如果希望在整个测试工作开始之前,进行一些准备工作,可以使用preload指令,它可以在bun test 中使用--preload标识,或者在项目配置中增加相关项目,如下面代码所示:
js
-- 命令参数
bun test --preload ./setup.ts
-- 配置文件 bunfig.toml
[test]
preload = ["./setup.ts"]
此外,有些流程化的测试,很多测试项目,是由前后逻辑或者数据间的依赖关系的,也需要很好的进行规划和组织。
此外需要注意,bun testing的执行顺序是,如果有多个文件,则按照文件名称顺序执行,而在执行文件内部,按照定义顺序来执行。
其他选项
在执行bun test的过程中,有一些可以控制的选项,开发者可以了解一下:
- 文件夹
默认情况下,bun test 会检查当前项目下,所有的测试项目。但也可以指定它只处理某一个文件夹下面的测试项目。特别是在开放阶段,开发者可以专注于开放和测试当前正在开放的项目和模块。
js
// just run file in test folder
bun test test/
- only
如果需要临时性的只运行某些测试项目,可以使用only标签,示例如下:
js
test("test #1", () => {
// does not run
});
test.only("test #2", () => {
// runs
});
describe.only("only", () => {
test("test #3", () => {
// runs
});
});
// 配套执行方式
bun test --only
run #2 and #3
- skip
如果想要忽略某些测试项目,可以使用skip标签。
- todo
todo的意思是,将测试涉及的项目,标记为将来可能要进行的测试工作。但当前的标准测试,不会执行这个项目。
- if/skipif/todoif
可以使用条件来控制测试项目是否执行,示例如下:
js
test.if(Math.random() > 0.5)("runs half the time", () => {
// ...
});
const macOS = process.arch === "darwin";
test.if(macOS)("runs on macOS", () => {
// runs if macOS
});
- failing
默认情况下,测试成功的依据是测试通过,而failing选项和正常情况是反的。出错了才会通过测试。
js
// This will pass because the test is failing as expected
test.failing("math is broken", () => {
expect(0.1 + 0.2).toBe(0.3); // fails due to floating point precision
});
// This will fail with a message that the test is now passing
test.failing("fixed bug", () => {
expect(1 + 1).toBe(2); // passes, but we expected it to fail
});
- each 批量测试参数
使用each可以为相同类型的测试传输不同的参数,进行批量数据的测试。
js
const cases = [
[1, 2, 3],
[3, 4, 7],
];
test.each(cases)("%p + %p should be %p", (a, b, expected) => {
expect(a + b).toBe(expected);
});
- timeout
可以为测试执行,或者某个测试方法的执行,设置超时时间。这个超时时间,可以在test方法的第三个参数中设置,jest的默认超时为5秒。
js
import { test } from "bun:test";
test("wat", async () => {
const data = await slowOperation();
expect(data).toBe(42);
}, 500); // test must run in <500ms
- Times
对于某些测试的场景,bun testing允许开发者设置当前的系统时间,来影响程序和应用的执行。
js
import { setSystemTime, beforeAll, test, expect } from "bun:test";
beforeAll(() => {
setSystemTime(new Date("2020-01-01T00:00:00.000Z"));
});
test("it is 2020", () => {
expect(new Date().getFullYear()).toBe(2020);
});
- watch
wartch 指令选项,可以控制bun test监控测试文件中引用的文件,当监测到变更时,自动执行测试工作。
- 环境变量
在没有指定的情况下,bun test 命令,会将当前的$NODE_ENV环境变量设置成为"test"。这也会影响一些需要检查执行环境的操作。
Converage 测试覆盖
测试覆盖,一共也是测试技术和测试工作的一项重要内容。它可以保证测试用例的编写和规划,是符合软件工程的设计目标的。经过测试的代码越多,覆盖率越高,表示测试工作越充分,相对而言对软件质量的保证越好。但需要注意的是,测试覆盖率高,并不简单的代表测试工作的质量越高,只能说它提供了一个更好的基础。除了简单的检查测试覆盖率,测试人员还需要精细的设计测试用例和方法,尽可能的覆盖所有的语句、执行的分支、数据组合、路径和条件,这些内容可能并不简单的体现在覆盖率上。
bun testing测试工作中,评估测试覆盖率的操作非常简单,就是在执行时,增加一个 --coverage标记,随后的测试报告中,就会展示相关测试文件和项目的测试覆盖率,如下图所示:

无论怎样,这个报告可以最直观的展示,代码中有哪些内容和方法没有被测试,可以帮助测试人员改进和完善测试方法。
Mock 模拟
这一部分的内容,特别是一些理论概念和技术,笔者也是在逐步理解和体会中。可能描述的不是非常切实和规范。
Mock的原意应当就是模拟的意思。Mock也是软件测试中的一种关键技术,它通过创建虚拟对象(即Mock对象)来替代真实的软件和系统依赖比如数据库、API、第三方服务等等,从而使测试过程能够隔离目标代码、控制测试环境并模拟各种场景。通过Mock技术,可以显著提高测试效率和场景覆盖程度,并能够优化开发和测试的业务流程。
Mock的核心技术原理包括:
- 隔离(Isolation)
利用Mock可以在测试时,分离被测试的代码和外部依赖,使开发测试能够专注于核心业务。其原因是,可能在测试的阶段,某些外部依赖(比如支付系统)条件不允许或者不方便使用,但又需要相关流程和数据。
- 替代(Substitution)
用虚拟对象替换复杂或未实现的依赖,这样可以优化开发测试流程。如某些模块已经设计和规划完成,但尚未真实的实现,就可以先通过Mock来进行替代,并完成正常的集成测试过程。
- 模拟 (Simulation)
使用Mock可以动态定义依赖的行为,比如返回特定数据、模拟抛出异常等等,来覆盖正常测试可能无法触达的场景。比如模拟支付接口超时,验证系统是否触发重试机制。
我们可以通过一些示例代码来理解这一机制:
js
import { test, expect, mock } from "bun:test";
const random = mock(() => Math.random());
test("random", async () => {
const val = random();
expect(val).toBeGreaterThan(0);
expect(random).toHaveBeenCalled();
expect(random).toHaveBeenCalledTimes(1);
});
除了mock,bun testing还支持一种spyon的模拟方式。 从名字上来看,就是设置一个程序的"间谍",来监视代码的执行。它和mock相比,是不需要定义一个mock,就可以跟踪代码的调用和执行。下面是相关的示例代码:
js
import { test, expect, spyOn } from "bun:test";
const ringo = {
name: "Ringo",
sayHi() {
console.log(`Hello I'm ${this.name}`);
},
};
const spy = spyOn(ringo, "sayHi");
test("spyon", () => {
expect(spy).toHaveBeenCalledTimes(0);
ringo.sayHi();
expect(spy).toHaveBeenCalledTimes(1);
expect(spy).toHaveBeenCalledTimes(2); // test false
});
Snapshot 快照
这也是一个比较有趣的特性,但笔者尚未理解到如何更好的使用它。按照官方技术文档的说法,如果选择使用"快照"模式来执行测试,测试框架将会在一个名为 __ snapshots __ 的文件夹中保存expect的参数。随后的测试过程,会比较当前测试和保存测试的结果,作为一个测试项目。这样开发中就可以比较不同测试过程中,相关状态和信息的变化了。
如下面的代码所示:
js
import { test, expect } from "bun:test";
test("snap", () => {
expect("foo").toMatchSnapshot();
});
// 重新生成快照
bun test --update-snapshots
也可以检查错误信息的快照:
js
import { test, expect } from "bun:test";
test("error snapshot", () => {
expect(() => {
throw new Error("Something went wrong");
}).toThrowErrorMatchingSnapshot();
expect(() => {
throw new Error("Another error");
}).toThrowErrorMatchingInlineSnapshot();
});
性能测试
bun testing的测试用例执行之后的测试报告中,也会简单的展示每个测试方法执行花费的时间,可以一定程度上衡量应用系统执行的性能。但显然,这个指标并不是很全面,因为它基本上就是在测试环境中的单一执行的过程,只能发现最大最严重的执行问题。
bun testing本身没有提供更专业的压力测试方法,这方面我们一般使用第三方工具来完成。比如笔者常用的autocannon,来对一个HTTP API进行压力和性能测试,如下图所示:

关于压力和性能测试,其实相对而言是另外一个领域的问题,这里只是简单提及,不再展开探讨。
小结
在本章节中,笔者讨论了Bun内置的测试框架:bun testing的相关技术和内容。文章先小结了软件测试的相关概念和方法论,然后探讨了bun的实现方式,包括基本形式,测试文件的构成,测试执行和报告的内容,断言的方式,生命周期管理和其他选项。最后简单讨论了软件测试中的mock和snapshoot等方面的内容。