9.1 测试:对照React Testing Library的组件测试


9.1 测试:对照 React Testing Library 的组件测试

本节摘要:Solid 的测试方案刻意对齐 React Testing Library 的哲学与查询词汇:渲染、查询、断言、用户事件四步一样不少,迁移者的肌肉记忆大半有效。真正的差异在两处——状态更新是微任务异步的,断言前要等;原语测试需要显式创建作用域。本节给出组件与原语两套测试模板。

测试可能是迁移成本最低的一环,因为核心思想是跨框架共识:测行为不测实现——用户看得见的才断言,内部状态一概不碰。React Testing Library 把这套哲学做成了 API,Solid 的官方测试库直接沿用了同一套查询词汇:按文字找、按角色找、按标签找,命令名几乎不变。团队测试文化可以直接平移。

组件测试:同一用例两份写法

需求:计数按钮,点击后数字加一,到上限禁用。React 版本:

import { render, screen } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; test("点击后计数加一", async () => { render(<Counter max={2} />); const btn = screen.getByRole("button"); await userEvent.click(btn); expect(screen.getByText("1")).toBeInTheDocument(); }); test("到上限禁用", async () => { render(<Counter max={1} />); const btn = screen.getByRole("button"); await userEvent.click(btn); expect(btn).toBeDisabled(); });

Solid 版本:

import { render, screen } from "@solidjs/testing-library"; import userEvent from "@testing-library/user-event"; test("点击后计数加一", async () => { render(() => <Counter max={2} />); const btn = screen.getByRole("button"); await userEvent.click(btn); expect(screen.getByText("1")).toBeInTheDocument(); }); test("到上限禁用", async () => { render(() => <Counter max={1} />); const btn = screen.getByRole("button"); await userEvent.click(btn); expect(btn).toBeDisabled(); });

逐处找不同:渲染函数从按组件调用变成传函数体——render(() => <Counter/>,这个差别源于 Solid 组件本来就是立即执行的函数,包一层延迟求值才能让测试库接管清理;测试库换成了 Solid 官方适配版;查询与断言 API 一字未改。还有一个看不见的差异藏在 await 里:点击处理器改了信号,绑定函数的执行排在微任务里,userEvent.click 内置了等待,所以两份代码的 await 位置一致;若你手动 fireEvent 或直接改状态,就需要显式等一轮微任务再断言——这是 Solid 测试里最常见的"为什么断言失败"。

原语测试:显式作用域

组合原语(3.3 的本地存储、5.2 的数据封装)的测试要脱离组件进行,此时需要手动创建作用域,把原语挂在 createRoot 下:

import { createRoot } from "solid-js"; import { createLocalStorage } from "./localStore"; test("跨标签同步写入", async () => { let setValue; createRoot(dispose => { const [value, set] = createLocalStorage("cart", []); setValue = set; // 模拟另一个标签页发来的存储事件 window.dispatchEvent(new StorageEvent("storage", { key: "cart", newValue: JSON.stringify([1, 2]), })); dispose(); // 作用域销毁,验证无泄漏 }); expect(setValue).toBeTruthy(); });

两个动作是 React 没有的仪式:createRoot 提供人工 Owner(4.2 讲过),dispose 显式销毁——销毁后断言订阅已释放,顺带完成一次内存泄漏检查。React Hook 测试的 renderHook 工具在 Solid 里有对应物,内部做的正是这两步。

对照表与流水线接入

维度 React Testing Library Solid 测试库
渲染入口 render(<A/>) render(() => <A/>)
查询 API getBy、findBy、queryBy 系列 同名同语义
用户事件 userEvent 共用 同一库直接复用
异步时序 act 机制 微任务冲刷,等一轮即可
Hook 测试 renderHook createRoot 加 dispose 仪式
清理 自动 自动(每用例独立作用域)

接入流水线没有新知识:Vitest 跑测试、jsdom 提供环境、覆盖率工具通用,配置与 React 项目同构。CI 上的检查组合建议是"类型检查加 lint 加测试"三件套,与 8.2 的规范清单配套。

⚠️ 测试里最危险的写法是绕过界面直接断言信号值——那是测实现,重构成 Memo 或 store 后测试全红,红灯却不代表行为变化。坚持从界面进:找到按钮、点它、看屏幕。

本节要点回顾

  • 查询词汇零迁移:测试哲学与 API 与 RTL 同源,肌肉记忆有效。
  • 渲染传函数体:立即执行的组件需要包一层延迟求值。
  • 微任务时序:信号更新后绑定异步冲刷,断言前要等。
  • 原语测试两个仪式:createRoot 给作用域,dispose 验证回收。

下一节看调试——当测试没拦住问题时,怎么用"依赖图之眼"找到病灶。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U