← [ BACK_TO_NOTES ]

Testing Library 查询 API

系统整理 Testing Library 的 getBy/queryBy/findBy、AllBy、ByRole 与查询优先级,并说明 presence、absence、async 与 scoped query 的选择规则。

#concept #growing #test #react #typescript

[!info] related notes

  • 所属 MOC: testing-moc
  • 前置概念:
  • 并列概念:
  • 易混淆概念:
  • 关系笔记:

Testing Library 查询 API

核心模型

Testing Library 的 Queries 用来回答:

用户现在能在界面上找到什么?

查询 API 可以沿两条轴理解:

第一条轴:找到 1 个还是多个?
By     → 单个
AllBy  → 多个

第二条轴:现在找、允许没有、还是异步等?
get    → 现在必须有
query  → 允许没有
find   → 异步等待出现

组合后就得到:

getBy...       getAllBy...
queryBy...     queryAllBy...
findBy...      findAllBy...

getBy...:现在必须存在一个

const button = screen.getByRole("button", { name: "保存" });

行为:

  • 找到恰好一个 → 返回 element
  • 一个都没有 → 立即抛出带 DOM 信息的错误
  • 找到多个 → 立即抛错,提示应使用更精确条件或 getAllBy...

适合:

expect(screen.getByText("疼痛")).toBeInTheDocument();

这里实际常常不需要 toBeInTheDocument() 才能证明“存在”,因为 getBy... 找不到已经会失败;但 matcher 会让测试意图更明确。

queryBy...:检查元素可能不存在

const button = screen.queryByRole("button", { name: "后来改善" });

行为:

  • 找到一个 → 返回 element
  • 一个都没有 → 返回 null
  • 找到多个 → 抛错

它最典型的用途是 absence assertion:

expect(
  screen.queryByRole("button", { name: "后来改善" }),
).not.toBeInTheDocument();

为什么不能用 getBy... 检查不存在

错误写法:

expect(
  screen.getByRole("button", { name: "后来改善" }),
).not.toBeInTheDocument();

如果元素不存在,getByRole() 会在 expect() 得到返回值之前就 throw,所以否定 matcher 根本来不及执行。

记忆:

要证明“没有”,用 query。

findBy...:异步等待元素出现

const message = await screen.findByText("保存成功");

findBy... 返回 Promise,并在一段时间内重复查询 DOM。

适合:

点击
  ↓
异步 mutation / state update
  ↓
稍后才出现 UI

例如:

await user.click(screen.getByRole("button", { name: "保存" }));
expect(await screen.findByText("已保存")).toBeInTheDocument();

如果超时仍未满足条件,则测试失败。

记忆:

现在应该有 → get;应该没有 → query;等一下应该有 → find。

getAllBy / queryAllBy / findAllBy

当业务上本来就应该有多个匹配元素时使用 All 版本。

const items = screen.getAllByRole("listitem");
expect(items).toHaveLength(3);

语义:

Query0 个1+ 个Async
getAllBy...throwarray否
queryAllBy...[]array否
findAllBy...超时后 throwPromise是

不要为了绕过“找到多个”的报错而机械换 getAllBy;如果界面逻辑只应该有一个按钮,找到两个恰恰可能暴露真实 bug。

查询条件:优先用户和可访问性语义

Testing Library 推荐优先使用接近用户定位界面的方式。

常见优先顺序:

  1. ByRole
  2. ByLabelText
  3. ByPlaceholderText
  4. ByText
  5. ByDisplayValue
  6. ByAltText
  7. ByTitle
  8. ByTestId

核心不是死背顺序,而是:

优先选择用户真实能够感知的语义,而不是实现专用 selector。

ByRole:通常是第一选择

screen.getByRole("button", { name: "确认记录" });

role 来自 accessibility tree。

常见元素天然具有隐式 role:

<button>保存</button>        → button
<a href="/x">详情</a>       → link
<input type="checkbox" />  → checkbox
<h2>身体记录</h2>            → heading

通常不要仅仅为了测试给标准 HTML 元素额外加重复 role。

name 是 accessible name

screen.getByRole("button", { name: "保存" });

name 不一定只是元素内部 text,它可能来自:

  • 按钮文本
  • <label>
  • aria-label
  • aria-labelledby
  • 图片 alt 等 accessibility 计算

因此 ByRole + name 同时测试了交互语义和可访问名称。

常用 ByRole filters

screen.getByRole("heading", { level: 2 });
screen.getByRole("checkbox", { checked: true });
screen.getByRole("tab", { selected: true });
screen.getByRole("button", { pressed: true });
screen.getByRole("treeitem", { expanded: true });

还有 current、busy、value、description 等过滤条件。

ByLabelText:表单控件非常自然

<label htmlFor="email">邮箱</label>
<input id="email" />

测试:

screen.getByLabelText("邮箱");

这比:

container.querySelector("#email")

更接近用户理解表单的方式,并顺便保护 label 与 input 的可访问关联。

ByPlaceholderText

screen.getByPlaceholderText("记录内容");

适合界面确实主要用 placeholder 提示用户的场景。

但如果有正式 label,优先 label;placeholder 通常不是最佳的可访问名称替代品。

ByText

screen.getByText("日常活动以久坐为主");

适合:

  • paragraph
  • status 文案
  • 普通展示文本

对于 button、input、checkbox 等有明确语义的可交互元素,优先 ByRole。

ByDisplayValue

适合验证表单当前值:

screen.getByDisplayValue("右肩疼痛");

但如果你已经拿到 input element,通常更直接:

expect(input).toHaveValue("右肩疼痛");

ByAltText

主要用于:

<img alt="BodySense 身体模型" />
screen.getByAltText("BodySense 身体模型");

ByTitle

可以查 title 属性,但 title 往往不是用户最主要的交互语义,所以优先级较低。

ByTestId:最后手段

<div data-testid="body-state-row" />
screen.getByTestId("body-state-row");

它很稳定,但本质是专门为测试提供的实现标识,用户并不知道 body-state-row。

适合:

  • 元素没有合理的 role/text/label
  • 复杂 canvas / visualization 容器
  • 语义查询无法稳定定位,但确实需要测试这个节点

不要因为写起来方便就把所有查询都变成 test id。

Exact string、Regex 与 matcher

screen.getByRole("button", { name: "保存" });
screen.getByRole("button", { name: /保存/ });
screen.getByRole("button", { name: /保存/i });

Exact string 更明确;Regex 适合:

  • 文案中包含动态数字
  • 大小写不重要
  • 只关心稳定语义片段

不要用过宽的 /./、/确认/ 让多个完全不同元素都可能匹配。

screen 与 render 返回的 queries

两种都可以:

const { getByRole } = render(<Component />);
getByRole("button", { name: "保存" });

或:

render(<Component />);
screen.getByRole("button", { name: "保存" });

多数测试推荐 screen,因为:

  • 少解构
  • 查询方式统一
  • 更像“从页面上找”

within():把查询限制在某一块 DOM

当页面有多个相同按钮时,先定位业务区域,再在区域内部查询:

const card = screen.getByRole("region", { name: "右肩" });
const save = within(card).getByRole("button", { name: "保存" });

这比使用:

第 2 个按钮
第 4 个 div

稳定得多。

Async queries 与 waitFor

首选 findBy:等待一个元素出现

expect(await screen.findByText("已保存")).toBeInTheDocument();

waitFor:等待任意断言最终成立

await waitFor(() => {
  expect(api.save).toHaveBeenCalledOnce();
});

waitFor 会重复执行 callback,直到不再 throw 或超时。

因此 callback 里应该放 assertion,而不是把异步 side effect 本身反复执行:

// ❌ 每次 retry 都可能再次点击
await waitFor(async () => {
  await user.click(button);
  expect(...);
});

应先 Act,再 wait/assert。

waitForElementToBeRemoved

适合等待 loading / dialog 等元素消失:

await waitForElementToBeRemoved(() => screen.queryByText("加载中"));

Presence、Absence、Appearance、Disappearance 四种模式

Presence
现在应该有
→ getBy

Absence
现在应该没有
→ queryBy

Appearance
异步后应该出现
→ findBy

Disappearance
异步后应该消失
→ waitForElementToBeRemoved / waitFor + queryBy

这是比死背所有 API 更稳定的选择方式。

BodyState 示例

当前 fact 必须显示

expect(
  screen.getByText("日常活动以久坐为主"),
).toBeInTheDocument();

Lifestyle 不允许 symptom action

expect(
  screen.queryByRole("button", { name: "后来改善" }),
).not.toBeInTheDocument();

用户确认

await user.click(
  screen.getByRole("button", { name: "确认记录" }),
);

等待异步 UI

expect(
  await screen.findByText("记录已确认"),
).toBeInTheDocument();

只有最后一个场景真正需要 findBy。

常见错误

用 queryBy 做应该存在的断言

expect(screen.queryByText("疼痛")).toBeInTheDocument();

虽然能工作,但失败信息通常不如 getByText 清楚,而且掩盖了“现在必须存在”的语义。

用 getAllBy 隐藏重复元素 bug

如果业务要求只有一个“提交”按钮,就应该让 getByRole 在出现两个时失败。

过度依赖 CSS selector

container.querySelector(".mt-2 > button:nth-child(3)")

这是典型的实现细节耦合。

无条件使用 findBy

所有查询都 await findBy... 会让测试不必要地等待,也模糊同步/异步边界。

最短记忆方式

get = 必须有;query = 可以没有;find = 等它有。

再加一条:

先 Role,再考虑 Text;最后才 TestId。

创建于 2026/8/31 更新于 2026/8/31
Digital Biome
Access

键盘导航

j / k / ↑ / ↓
选择对象
Enter / o
打开选择
/ 或 Ctrl+K
全站搜索
i / f / 1–4
筛选当前列表
g → h / l / p / t / a
首页 / 笔记 / 项目 / 工具 / About
r → 1–9
打开所选对象的关联
u
返回上一页
Esc
关闭浮层 / 取消命令;输入框内清空文本