Bun技术评估 - 10 Testing

概述

本文是笔者的系列博文 《Bun技术评估》 中的第十篇。

本文主要探讨的内容,评估和理解,bun是如何支持和实现开发工作的一个非常重要的环节,就是Testing(测试)的。和很多其他的模块类似,bun也内置了test支持相关的模块和功能,可以直接使用。

关于这方面的内容,主要参考了bun官方技术文档的相关章节,其链接如下:

bun.sh/docs/cli/te...

关于测试

首先需要说明,笔者的主要工作并非测试相关,原来对测试技术也不甚精通,包括在自己的日常开发过程中,相关的理解和应用也不是很多。但之所以考虑在本系列文章中,规划和增加了讨论测试相关的内容,主要是由于看到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等方面的内容。

相关推荐
Carlos_sam1 小时前
OpenLayers:封装一个自定义罗盘控件
前端·javascript
前端南玖1 小时前
深入Vue3响应式:手写实现reactive与ref
前端·javascript·vue.js
郑道2 小时前
Docker 在 macOS 下的安装与 Gitea 部署经验总结
后端
3Katrina2 小时前
妈妈再也不用担心我的课设了---Vibe Coding帮你实现期末课设!
前端·后端·设计
汪子熙2 小时前
HSQLDB 数据库锁获取失败深度解析
数据库·后端
高松燈2 小时前
若伊项目学习 后端分页源码分析
后端·架构
没逻辑2 小时前
主流消息队列模型与选型对比(RabbitMQ / Kafka / RocketMQ)
后端·消息队列
Yueyanc2 小时前
LobeHub桌面应用的IPC通信方案解析
前端·javascript
倚栏听风雨3 小时前
SwingUtilities.invokeLater 详解
后端
Java中文社群3 小时前
AI实战:一键生成数字人视频!
java·人工智能·后端