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等方面的内容。

相关推荐
良许Linux10 分钟前
DSP的选型和应用
后端·stm32·单片机·程序员·嵌入式
起风的蛋挞12 分钟前
Matlab提示词语法
前端·javascript·matlab
不光头强18 分钟前
spring boot项目欢迎页设置方式
java·spring boot·后端
怪兽毕设32 分钟前
基于SpringBoot的选课调查系统
java·vue.js·spring boot·后端·node.js·选课调查系统
Amumu1213843 分钟前
Vue Router(一)
前端·javascript·vue.js
学IT的周星星1 小时前
Spring Boot Web 开发实战:第二天,从零搭个“会卖萌”的小项目
spring boot·后端·tomcat
2603_949462101 小时前
Flutter for OpenHarmony 社团管理App实战 - 资产管理实现
开发语言·javascript·flutter
郑州光合科技余经理1 小时前
可独立部署的Java同城O2O系统架构:技术落地
java·开发语言·前端·后端·小程序·系统架构·uni-app
VT.馒头1 小时前
【力扣】2694. 事件发射器
前端·javascript·算法·leetcode·职场和发展·typescript