Bun test 常见问题

前序文章 用 Bun 🍞 给 AI 应用做自动化集成测试 的补充。

1. 如何提升测试性能?

  1. getAllByRole 改成 getAllByText,性能从 2.6s 优化到 <1ms。

  2. 我们对文字断言也做了性能优化,从 1s+ 优化到零点几ms!这是因为我们的 HTML 很大性能是瓶颈,如果你的 HTML 不是很大,优先 innerText 而非 textContent,原因上文有讲到。

  3. 还可以继续对 parse HTML 优化,使用性能更好的 node-html-parser,估计可以从几百毫秒优化到几十毫秒,但是收益相对不大暂时不优化。

  4. 利用 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 由三部分组成,

  1. renders Chat App - 成功从 localStorage 渲染首屏历史对话: test title
  2. formatted innerHTML toMatchSnapshot 的入参
  3. 1 自动生成,防止同名冲突

更多阅读:bun.sh/docs/test/s...

5. snapshot 失败了但是看不出问题

长文本 snapshot 对比很不明显,这也是 bun 需要改善的地方。

两种方法

  1. 改成数组:split('\n')
  2. 增加 --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)模式

react.dev/reference/r...

断言之前先 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.report/1.1.29/et16...

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 但是 platformMINGW64 才会进入 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/installinstall.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 the Bun global and all bun:* 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 希望官方能给出一个全局阈值的配置。

相关推荐
gb421528713 分钟前
springboot项目下面的单元测试注入的RedisConnectionFactory类redisConnectionFactory值为什么为空呢?
spring boot·后端·单元测试
PyAIGCMaster2 小时前
react+taro 开发第五个小程序,解决拼音的学习
react.js·小程序·taro
前端达人4 小时前
React 播客专栏 Vol.18|React 第二阶段复习 · 样式与 Hooks 全面整合
前端·javascript·react.js·前端框架·ecmascript
testleaf11 小时前
React知识点梳理
前端·react.js·typescript
每天都有好果汁吃12 小时前
基于 react-use 的 useIdle:业务场景下的用户空闲检测解决方案
前端·javascript·react.js
天天扭码12 小时前
面试必备 | React项目的一些优化方案(持续更新......)
前端·react.js·面试
萌萌哒草头将军13 小时前
🏖️ TanStack Router:搜索参数即状态!🚀🚀🚀
javascript·vue.js·react.js
咔咔库奇15 小时前
开发者体验提升:打造高效愉悦的开发环境
前端·javascript·vue.js·react.js·前端框架
红衣信15 小时前
从原生 JS 到 Vue 和 React:前端开发的进化之路
前端·vue.js·react.js
yanhhhhhh17 小时前
React中的hydrateRoot
react.js