
前序文章 用 Bun 🍞 给 AI 应用做自动化集成测试 的补充。
1. 如何提升测试性能?
-
将
getAllByRole
改成getAllByText
,性能从 2.6s 优化到 <1ms。 -
我们对文字断言也做了性能优化,从 1s+ 优化到零点几ms!这是因为我们的 HTML 很大性能是瓶颈,如果你的 HTML 不是很大,优先
innerText
而非textContent
,原因上文有讲到。 -
还可以继续对 parse HTML 优化,使用性能更好的 node-html-parser,估计可以从几百毫秒优化到几十毫秒,但是收益相对不大暂时不优化。
-
利用
within
将 query 范围限定在某一个元素内,让 query 最精准也能顺带提升性能,最重要是能利用 RTL 提供的 API。
2. 为何我的 mock adapter 不生效
假设你的业务代码如下,封装了一个 axios 实例用来做返回值的拦截处理:
ts
// src/utils/http.ts
import axios from 'axios'
class Request {
// axios 实例
instance: AxiosInstance
constructor(config: AxiosRequestConfig) {
this.instance = axios.create(config)
// 全局响应拦截器保证最后执行
this.instance.interceptors.response.use(
...
}
request<T, P>(config: AxiosRequestConfig): Promise<T | P> {
return this.instance.request(...)
}
}
const config: AxiosRequestConfig = {
// 默认地址
// baseURL: '/neuron',
baseURL: '',
// 设置超时时间
timeout: 30000,
// 跨域时候允许携带凭证
withCredentials: true,
}
const instance = new Request(config)
而你的 mock 是这么写的:
ts
// foo.test.ts
const axios = require('axios');
const MockAdapter = require('axios-mock-adapter');
const mock = new MockAdapter(axios);
那么 mock 是不会生效的,因为业务代码使用的是 axios 实例,而你 mock 的是整个 axios 模块。 怎么解决?
第一步导出实例
ts
// src/utils/http.ts
// 请勿删除 export 否则单元测试无法 mock axios
export const instance = new Request(config)
第二步
ts
// foo.test.ts
import { instance } from './utils/http'
const mock = new MockAdapter(instance.instance);
3. error: useLocation() may be used only in the context of a <Router>
component.
将组件包裹在 Router
中即可,单测推荐使用 MemoryRouter
。
4. 什么是 snapshot test 和其变种 inline snapshot 以及什么时候用?
snapshot
翻译即"快照",就是给测试对象在某个时刻"拍一张照"。这个"对象"可以是字符串(HTML、普通字符串)、数组或对象,其实任何 JS value 都行。当要断言复杂 UI 组件、大对象的时候特别有用。想一想如果要断言一个复杂组件生成到 HTML
结构,若要逐个写 screen.getByText ...
是不是想一下都头皮发麻。
故当输出结果很长手动写特别麻烦的时候可以借助 snapshot
,测试框架会自动在 __snapshot__
文件夹下生成对应测试过文件的 *.snap
文件。如果是 inline snapshot
则不会生成文件而是在调用处自动生成。那 inline
有什么用呢?首先 inline
一般用于不是很长的断言,其次 inline
后我们可以很方便的将其改成 toEqual
等断言方式,为什么改?修改后维护和 diff
更方便。
我们来看看一个 snapshot test 的例子:
假设有 openApp.test.tsx
ts
test('renders Chat App - 成功从 localStorage 渲染首屏历史对话', async () => {
const formatted = await toStableHTML(screen.getByRole('application').innerHTML)
expect(formatted).toMatchSnapshot('formatted innerHTML')
...
执行 bun test openApp.test.tsx
后自动在 __snapshots__
文件夹下生成 snapshot 文件 openApp.test.tsx.snap
sh
❯ tree src/__snapshots__
__snapshots__
└── openApp.test.tsx.snap
内容
plain
// Bun Snapshot v1, https://goo.gl/fbAQLP
exports[`renders Chat App - 成功从 localStorage 渲染首屏历史对话: formatted innerHTML 1`] = `
"<div class="headerDiv">
<div class="headerLeft">
<img src="...DISK_PATH/loginTitleIcon.png" />
...
解读:每一个 snapshot 断言的 title 由三部分组成,
renders Chat App - 成功从 localStorage 渲染首屏历史对话:
test titleformatted innerHTML
toMatchSnapshot 的入参1
自动生成,防止同名冲突
5. snapshot 失败了但是看不出问题
长文本 snapshot 对比很不明显,这也是 bun 需要改善的地方。
两种方法
- 改成数组:
split('\n')
- 增加
--update-snapshots
比如bun test src/openApp.test.tsx --update-snapshots
。更新后 git diff 即可,详见 bun.sh/docs/test/s...
6. Warning: An update to Provider inside a test was not wrapped in act(...).
When writing UI tests, tasks like rendering, user events, or data fetching can be considered as "units" of interaction with a user interface. React provides a helper called
act()
that makes sure all updates related to these "units" have been processed and applied to the DOM before you make any assertions.在编写 UI 测试时,诸如渲染、用户事件或数据获取等操作都可以视为与用户界面的"交互单元"。React 提供了一个名为
act()
的辅助工具,确保在进行任何断言之前,所有与这些"交互单元"相关的更新都已处理并应用到 DOM。
act()
的名称来源于 Arrange-Act-Assert(AAA)模式
断言之前先 await act
让状态变化先应用到 DOM
上,这样才能方便做 DOM
的断言。
diff
import { render, screen } from '@testing-library/react'
- render(<OpenApp></OpenApp>)
+ await act(() => {
+ return render(<OpenApp></OpenApp>)
+ })
7. bun test toMatchSnapshot 报错 "panic(main thread): integer overflow"
如果针对整个容器断言,bun 将崩溃。
tsx
const { container } = render(<OpenApp></OpenApp>)
expect(container).toMatchSnapshot()
解法:改成 HTML 或 textContent。
详细报错和排查过程:
❯ bun test src/openApp.test.tsx
bun test v1.1.29 (6d43b366)
src\openApp.test.tsx: Warning: KaTeX doesn't work in quirks mode. Make sure your website has a suitable doctype.
============================================================ Bun v1.1.29 (6d43b366) Windows x64 (baseline) Windows v.win10_fe CPU: sse42 avx avx2 Args: "E:\pnpm\global\5\node_modules\bun\bin\bun.exe" "test" "src/openApp.test.tsx" Features: jsc bunfig dotenv transpiler_cache(30) tsconfig_paths(2) tsconfig(24) Builtins: "node:assert" "node:buffer" "node:child_process" "node:constants" "node:crypto" "node:events" "node:fs" "node:http" "node:https" "node:net" "node:os" "node:path" "node:perf_hooks" "node:stream" "node:stream/web" "node:string_decoder" "node:tty" "node:url" "node:util" "node:util/types" "node:vm" "node:zlib" Elapsed: 3523ms | User: 3015ms | Sys: 1484ms RSS: 0.55GB | Peak: 0.55GB | Commit: 0.70GB | Faults: 170929
panic(main thread): integer overflow oh no: Bun has crashed. This indicates a bug in Bun, not your code.
To send a redacted crash report to Bun's team, please file a GitHub issue using the link below:
bun --version 1.1.29,尝试升级到 1.2 仍然崩溃,已经提交 issue github.com/oven-sh/bun... 。
升级过程
首先查看 bun 是被谁安装的:
bash
❯ which bun
/e/pnpm/bun
lua
❯ pnpm i -g bun
EPERM EPERM: operation not permitted, open 'e:\pnpm\bun'
❯ pnpm self-update
EBUSY EBUSY: resource busy or locked, open 'E:\pnpm\pnpm'
但是报错无权限。改成用 administrator 角色打开 terminal 虽然可以安装成功,但是
perl
❯ pnpm i -g bun
WARN 4 deprecated subdependencies found: [email protected], [email protected], [email protected], [email protected]
Already up to date
Progress: resolved 93, reused 84, downloaded 0, added 0, done
Done in 1.9s
执行 bun -v
报错:
bash
❯ bun -v
/c/Program Files/nodejs/bun: line 12: C:\Program Files\nodejs/node_modules/bun/bin/bun.exe: No such file or directory
这是因为 bun 之前被 npm 安装过,需要 uninstall。如果不记得当时安装时候的 node.js 版本,可以 nvm 切换不同的 node.js 然后执行 uninstall 直到
❯ npm uninstall -g bun
removed 3 packages, and audited 2 packages in 226ms
found 0 vulnerabilities
如果还不行,进入目录删除 /c/Program Files/nodejs/bun
相关的任何 bun 文件(bun bun.exe bun.bat bun.ps1......)
再次执行 bun -v
若仍然不行。
切换安装方式
curl -fsSL https://bun.sh/install | bash
虽然我的操作系统是 Windows 10,但是因为安装了 git-bash。故仍然使用 Linux 安装方式(实在不想用 Windows 的包管理器)。
bash
curl -fsSL https://bun.sh/install | bash
为什么可以这么做?可以看下安装脚本 bun.sh/install:
bash
#!/usr/bin/env bash
set -euo pipefail
platform=$(uname -ms)
if [[ ${OS:-} = Windows_NT ]]; then
if [[ $platform != MINGW64* ]]; then
powershell -c "irm bun.sh/install.ps1|iex" # Windows 安装过程
exit $?
fi
fi
... # Linux 和 macOS 安装过程
bash
❯ uname -ms
MINGW64_NT-10.0-19045 x86_64
我们看看自己的 OS 和 platform 是什么。
sh
❯ echo $OS
Windows_NT
❯ uname -ms
MINGW64_NT-10.0-19045 x86_64
安装脚本有两重判断当 OS
等于 Windows_NT
但是 platform
非 MINGW64
才会进入 Windows 安装过程。我的电脑虽然是 Windows 但是安装了 git-bash(mingw)故不满足会走到 Linux 类系统安装过程。
新问题执行 curl -fsSL https://bun.sh/install | bash
会超时,因为默认通过 github 下载安装包 github.com/oven-sh/bun...
bash
❯ curl -fsSL https://bun.sh/install | bash
curl: (28) Failed to connect to github.com port 443 after 21044 ms: Couldn't connect to server
error: Failed to download bun from "https://github.com/oven-sh/bun/releases/latest/download/bun-windows-x64.zip"
那这个包地址是如何拼接而来的呢?如果我们能将 github 换成其镜像不就可以快速下载了吗?
替换 GITHUB 环境变量
首先 windows-x64 是根据 platform 而来的:
sh
case $platform in
'Darwin x86_64')
target=darwin-x64
;;
'Darwin arm64')
target=darwin-aarch64
;;
'Linux aarch64' | 'Linux arm64')
target=linux-aarch64
;;
'MINGW64'*)
target=windows-x64 # MINGW64 会下载 windows-x64
;;
'Linux x86_64' | *)
target=linux-x64
;;
esac
继续看:
sh
GITHUB=${GITHUB-"https://github.com"}
github_repo="$GITHUB/oven-sh/bun"
...
if [[ $# = 0 ]]; then
bun_uri=$github_repo/releases/latest/download/bun-$target.zip
else
bun_uri=$github_repo/releases/download/$1/bun-$target.zip
可以发现是通过 bun_uri=$github_repo/releases/latest/download/bun-$target.zip
拼接而来。
GITHUB=${GITHUB-"https://github.com"}
这句话的意思是如果环境变量存在 GITHUB 则使用,否则兜底 https://github.com
!bun 真是太贴心了,帮我们预留了"后门",据此我们可以通过环境修改 GITHUB。
sh
❯ GITHUB=https://xxgithubyy.zz curl -fsSL https://bun.sh/install | bash
########################################################################################################################## 100.0%
bun was installed successfully to ~/.bun/bin/bun
Run 'bun --help' to get started
xxgithubyy.zz 只是示例,大家换成自己的源
如果无法设置或者 bun 写死了怎么办?直接下载 bun.sh/install 为 install.sh
然后修改代码,最后执行bash install.sh
即可。
8. 生成的 snapshot 一行,不利于阅读和 diff
比如 expect(screen.getByRole('application').innerHTML).toMatchSnapshot('innerHTML')
.
解法:使用本文的 toStableHTML
。
9. Cannot find module 'bun:test' or its corresponding type declarations.ts(2307)
你只需做一件事!安装 @types/bun 即可,无需其他配置。
That's it! VS Code and TypeScript automatically load
@types/*
packages into your project, so theBun
global and allbun:*
modules should be available immediately.
详见 dgithub.xyz/oven-sh/bun... 和 dgithub.xyz/oven-sh/bun...
10. bun test 和 vitest 输出结果的区别
对于颜色:bun 原样输出,vitest 会转成 rgb
比如
tsx
<span style={{ background: '#E8E8E8' }}>
{children}
</span>
如果二者都需要运行需要断言需写成:
tsx
expect(tag.style.backgroundColor).oneOf(['#E8E8E8', 'rgb(232, 232, 232)'])
对于 CSS 简写属性的处理:bun 将拆分,符合实际渲染,vitest 原样输出。
组件源码
tsx
style={{
border: `8px solid transparent`,
borderTopColor: color,
borderRightColor: color,
}}
bun test 和 vitest 输出差异:
diff
- bun test
+ vitest
[
- "border-width: 8px",
- "border-style: solid",
- "border-color: #2C79FF #2C79FF transparent transparent",
- "border-image: initial",
+ "border: 8px solid transparent",
+ "border-top-color: #2C79FF",
+ "border-right-color: #2C79FF",
"position: absolute",
"top: 4px",
"right: 4px",
]
解法 toContainEqual
:
ts
// bun result
const bunResult = [
// 不同部分
'border-width: 8px',
'border-style: solid',
'border-color: #2C79FF #2C79FF transparent transparent',
'border-image: initial',
// 相同部分
'position: absolute',
'top: 4px',
'right: 4px',
]
// vitest result
const vitestResult = [
// 不同部分
'border: 8px solid transparent',
'border-top-color: #2C79FF',
'border-right-color: #2C79FF',
// 相同部分
'position: absolute',
'top: 4px',
'right: 4px',
]
expect([bunResult, vitestResult]).toContainEqual(result)
DRY 一下:
ts
expect(
[
// bun test
[
'border-width: 8px',
'border-style: solid',
'border-color: #2C79FF #2C79FF transparent transparent',
'border-image: initial',
],
// vitest
['border: 8px solid transparent', 'border-top-color: #2C79FF', 'border-right-color: #2C79FF'],
].map((arr) => {
// 相同部分
return arr.concat(['position: absolute', 'top: 4px', 'right: 4px'])
}),
).toContainEqual(result)
11. 为什么已高于整体阈值,测试仍然失败?
一句话:bunfig.toml 的 coverageThreshold
非全局阈值。
coverageThreshold
的配置 仅针对单个文件 (而非全局平均值)。即使整体覆盖率达标,只要有一个文件的覆盖率低于阈值,Bun 就会标记测试失败。
bun 对质量更严苛,此举也确实是为了保证质量,但是过于激进,作为默认配置不合理,已回复 issue 希望官方能给出一个全局阈值的配置。