跳转至正文

React Native Testing Library:如何正确测试 App

掌握 React Native Testing Library:涵盖配置、查询、Mock 和 CI 技巧。为组件、Hooks 和导航编写可靠的、以用户为中心的测试。

React Native Testing Library:如何正确测试 App

你的 React Native 测试套件显示全部通过,但用户却反馈在真机上点击“继续”没有任何反应。组件测试在模拟的 JavaScript 环境中找到了按钮、调用了处理函数,并看到了预期的界面。但它从未验证过原生权限 prompt、键盘行为、动画、平台 API 或实际的导航栈。

这种测试盲区正是让团队产生虚假安全感的地方。 React Native Testing Library 它非常适合测试组件的渲染效果以及对用户交互的响应,但它无法替代真机测试或性能测试。可靠的策略是利用每一层测试来暴露其对应的潜在问题。

目录

为什么以用户为中心的测试能改变一切

常见的测试失败往往在编写测试之前就已经埋下。开发者可能会检查组件的 props、深入其 state,或者对比庞大的 snapshot,因为这些细节很容易进行断言。测试通过了,但随后一次重构在不改变用户体验的情况下改变了组件结构,导致测试套件崩溃。更糟糕的是,由于断言从未描述用户真正需要看到的内容,即使用户可见的行为已经出错,测试可能依然在通过。

React Native Testing Library 采取了截然相反的方法。你渲染组件,通过可见的控件与其交互,然后对用户可以观察到的结果进行断言。这种方法符合 React Native Testing Library 以用户为中心的示例,同时也符合 React Native 的测试指南:保持测试简洁、让每个测试专注于单一目标、将视图关注点与业务逻辑及 state 分离,并优先使用可见输出或 accessibility helpers,而非内部实现细节。

一张展示以用户为中心的测试优势的图表,重点突出了用户行为、韧性、可靠性和更佳的实践方法。

测试用户依赖的行为

假设一个登录表单在请求运行时会禁用提交按钮。一个脆弱的测试可能会检查 disabled 针对特定的组件实例,或者断言某个状态变量已发生变化。一个更强大的测试会点击可访问的“Sign in”控件,等待加载指示器出现,并检查是否显示了错误消息或跳转到了目标页面。

第二个测试并不关心表单是使用本地 state、reducer、自定义 hook,还是使用了不同的按钮实现。它关心的是应用是否传达了正确的结果。

实用原则: 如果用户无法观察到某项内容,那么请质疑它是否应该包含在组件行为测试中。

基于无障碍标签(accessibility labels)和角色(roles)的查询也能促使更好的产品 code。一个提供有意义标签的界面不仅更容易配合辅助技术使用,也更容易在测试中进行验证。当你评估更广泛的 应用用户体验时,这种关联性至关重要,因为可测试性和可用性通常是相辅相成的。

了解测试通过并不代表什么

以用户为中心的组件测试可以证明 JavaScript 渲染了预期的分支并响应了模拟的点击。但它无法证明生物识别 prompt 是否能正确打开、原生相机是否返回了可用结果,或者支付流程能否应对特定平台的生命周期行为。

这种界限并非库本身的缺陷,而是保持测试层级真实性的原因。使用 RNTL 进行组件行为测试,而将基于设备的测试留给那些原生集成、真实导航、权限、身份验证、支付或核心应用功能可能会改变结果的流程。

真正有效的安装与配置指南

现代化的配置应从作用域包开始:

@testing-library/react-native

旧版的 react-native-testing-library npm 包名作为历史遗留仍存在,但当前项目应使用 Testing Library 家族维护的作用域包。该项目 GitHub 仓库 将其描述为旨在促进良好测试实践的 React Native 工具集,其发布历史表明,该库会随 React Native 同步演进,而非仅仅是一个静态辅助工具。

在现有的 Jest 环境中安装该库。Expo 项目通常使用 Expo Jest preset,而 bare React Native 项目则可能使用 React Native preset:

npm install --save-dev @testing-library/react-native jest

对于 Expo,请添加项目已有的 preset:

{
  "scripts": {
    "test": "jest"
  },
  "jest": {
    "preset": "jest-expo"
  }
}

对于 bare React Native 应用,请改用对应的 React Native Jest 配置。确保 preset 与框架版本保持一致。许多看似 RNTL 问题的问题,实际上是由 React renderer、Babel transform 或 Jest preset 不匹配导致的。

保持 setup 文件清晰明确

将共享的环境配置放在 setup 文件中,而不是在每个测试中重复编写 mocks:

// jest.setup.js
import '@testing-library/react-native/extend-expect';

然后在 Jest 中引用它:

{
  "jest": {
    "preset": "jest-expo",
    "setupFilesAfterEnv": ["<rootDir>/jest.setup.js"]
  }
}

如果你的应用使用了 React Navigation、Reanimated、gesture handling、safe-area context 或 storage 模块,请仅配置测试环境所需的 mock。全局 mock 可能会改变应用行为,虽然能让每个测试都更容易通过,但也会降低测试套件的可信度。

TypeScript 也需要同样的关注。确保 Jest 通过 preset 或你的 Babel 配置来转换 .ts 和 .tsx 文件,并让编译器能够识别测试类型。一个能在本地运行但未经过类型检查的测试,可能会掩盖错误的查询名称、无效的 navigation 参数或不安全的 mock 结构。

在诊断旧版配置建议时,查看发布时间线非常有用。该项目列出了 127 个版本,其中 v14.0.1 发布于 2026-06-23,而 v12.9.0 于 2024-11-27 发布,新增了对 React Native 0.77 和 Expo 52 的官方支持。2025 年 3 月的 v14 alpha 版本从已弃用的 React Test Renderer 迁移到了 Universal Test Renderer,并为仅支持 React 19 做好了准备。这些详情可以在 RNTL 发布历史,因此在不检查应用版本的情况下,切勿直接从过时的教程中复制渲染器依赖。

若要构建实用的 Jest 基础,请将您的配置与此进行对比: JavaScript 单元测试指南,然后在添加导航和原生 mock 之前,先运行一个小型组件测试。

展示设置 React Native Testing Library 项目五步安装流程的信息图。

核心 API:查询与断言详解

当 RNTL 测试的查询方式与用户的视觉、查找和操作行为一致时,测试才具有可信度。API 虽然很小,但如果选择暴露实现细节的选择器,可能会导致测试结果产生误导。

先使用 render:

const screen = render(<LoginForm />);

请选择与用户关联度最高的查询方式。如果组件提供了无障碍接口,优先使用面向无障碍(accessibility-oriented)的查询;如果行为基于文本,则使用可见文本;并将 testID 保留给那些没有明确用户可见选择器,或需要稳定的集成钩子(integration hook)的情况。

展示软件测试自动化中推荐查询优先级顺序的金字塔信息图。

根据时机和意图选择查询方式

每种查询方式都有其特定的用途:

  • 同步查询: 使用 getByRole, getByText,或其他 getBy 当元素理应已存在时使用。如果元素不存在,测试会立即失败。
  • 异步查询: 使用 findByRole 或 findByText 用于渲染或交互引起异步更新的情况。
  • 不存在性检查: 使用 queryByText 或 queryByTestId 当元素可能缺失,且预期结果为 null 而非抛出异常时。
  • 备选选择器: 使用 getByTestId 刻意为之。它为复杂的控件提供了持久的 hook,但不应取代应用中所有的无障碍标签。

对于聚焦事件测试, fireEvent.press 最为直接:

fireEvent.press(screen.getByRole('button', { name: 'Save' }));

使用 userEvent 当安装的版本支持更真实的交互序列时。无论如何,都要对最终的 UI 进行断言:

expect(await screen.findByText('Saved')).toBeTruthy();

处理器断言适用于以事件回调作为公共契约的组件。但若仅以此作为用户路径是否通畅的证据,则显得力度不足。

优先使用特定断言而非快照

优秀的断言应当描述屏幕内容:

expect(screen.getByText('Account created')).toBeTruthy();
expect(screen.getByRole('button', { name: 'Continue' })).toBeEnabled();

它们还可以验证无障碍状态、选中状态以及可见的校验反馈。避免检查内部实现细节:

expect(screen.getByTestId('submit-button').props.onPress).toBeDefined();

该断言只能证明 prop 是否存在,而不能证明功能是否正常。小而精细的快照可以捕捉结构变化,而大型导航或页面快照往往会产生大量噪音,导致难以察觉的更新被误通过。

Testing Library 软件包属于更广泛的 testing-library npm 组织。其活跃的软件包包括 @testing-library/react-native,版本号为 13.3.3,发布于 2026 年,根据该项目的 仓库信息。通用的查询规范有助于跨平台使用,但它们无法决定哪个断言能代表你的产品行为。

如需更广泛地比较 Jest 组件测试实践,请参阅此 React 单元测试指南。RNTL 仍然止步于 JavaScript 渲染的组件边界。原生权限、真实的导航栈、设备键盘、帧时序和内存行为需要 E2E 或性能测试工具,而不是更多的组件 mock。

下方视频演示了实际场景中的查询与断言工作流。

组件、Hooks 与导航的实用测试模式

一套实用的测试套件应遵循应用的架构。展示型组件需要直接的行为测试,Hooks 需要受控的输入与输出,导航需要足够真实的 Provider,而原生模块则需要与真机验证有明显区别的 Mock。

木质书桌上的现代笔记本电脑,屏幕显示 React Native code 和移动应用原型。

展示型组件

让组件测试贴近其公共契约:

const onSelect = jest.fn();

render(
  <PlanCard
    title="Team"
    description="Shared workspace"
    onSelect={onSelect}
  />
);

fireEvent.press(screen.getByRole('button', { name: 'Choose Team' }));

expect(onSelect).toHaveBeenCalled();

标签应与 UI 完全一致。关键在于测试应像用户或辅助功能服务那样查找控件,并确认重要的可见结果或回调结果。不要默认 Mock 所有子组件。仅在昂贵或无关的边界影响测试行为时才进行 Mock。

对于自定义 Hook,请使用 renderHook 当安装的 RNTL 版本提供该功能时:

const { result } = renderHook(() => useSearch());

await act(async () => {
  await result.current.submit('query');
});

expect(result.current.status).toBe('success');

Hook 测试应控制网络或仓库边界,而不是复现整个应用。应单独测试 Screen,以便确认 Hook 的状态是否能转化为有效的 UI。

对于导航行为,在真实的 NavigationContainer 且使用轻量级测试导航器,通常比 Mock 每个导航方法更有价值。点击可见控件,等待目标内容加载,并断言新页面的输出。通过直接 useNavigation 对于仅负责分发类型化路由的小按钮,使用 mock 仍然是合适的,但它无法验证路由注册、参数或嵌套导航器的行为。

异步数据也应遵循同样的规范。Mock API 或 repository 的响应,渲染屏幕,断言加载状态,resolve 请求,然后断言成功或错误输出。使用 findBy queries 来等待预期最终出现的元素,并显式处理被拒绝的请求,否则测试可能会因为组件从未进入预定分支而误判为通过。

原生模块 mock

针对 AsyncStorage、权限、摄像头、生物识别和平台 API 的 mock 对于确定性的 JavaScript 测试非常有用。但它们并不能证明原生功能正常工作。请确保 mock 行为贴近模块契约,在测试之间重置调用,并包含失败响应,而不仅仅是模拟 happy path。

场景 配合 RNTL 效果最佳 需要 E2E 验证
表单验证与可见错误 是 通常不需要
来自 mock repository 的加载、成功和错误 UI 是 针对关键的生产流程
已注册页面间的导航 可以,使用测试导航器 涉及手势、深层链接或平台行为时可以
AsyncStorage 状态决策 可以,使用受控 Mock 启动与持久化涉及原生生命周期时可以
摄像头、生物识别、权限或平台 API JS 回退与分支逻辑 可以,在真实或代表性设备上
布局、渲染性能及原生 code 否 是的,可以通过真机或专业工具实现

界限在于实践:通过 mock 依赖项来测试你的 JavaScript 逻辑,然后通过真机测试来确认依赖项的真实行为。

调试 CI 中的 Flaky Test 与性能检查

Flaky test 通常指向不受控的时间、共享状态或与 UI 竞争的断言。在增加超时时间之前,先识别具体原因。单纯延长超时时间可能会掩盖调度问题,并降低测试套件的运行速度。

使用 findBy 用于预期在更新后出现的元素。使用 waitFor 用于状态条件或 mock 调用。如果启用了 fake timers,请在交互所需的时刻推进时间,并在之后恢复真实计时器。出现 act 警告意味着 React 在预期的交互边界之外观察到了更新。修复缺失的 await、用户交互或 timer flush,而不是直接忽略警告。

让 CI 失败可复现

可靠的 CI 任务应通过 lockfile 安装依赖,运行与本地相同的 Jest 命令,并隔离 mock 状态。在测试之间清除 mock 调用,当模块级状态影响行为时重置模块,并消除对执行顺序的依赖。Jest 缓存可以加快反馈速度,但依赖项或配置变更需要进行相应的缓存失效处理。

仅在 CI 中出现的失败,在重写组件前需要进行环境对比。检查 Node、包管理器、Jest worker 设置、定时器配置以及环境变量。尽可能在本地复现相同的命令,然后将失败的测试缩减到能暴露差异的最小交互步骤。

可观测性(Observability)填补了不同的空白。 应用健康状况监控 提供了模拟组件测试无法复现的生产环境错误上下文,包括与真实设备和原生集成相关的失败。

涉及安全敏感的流程需要第二道防线。使用组件测试进行验证和状态转换,然后针对交接过程和原生行为增加设备级覆盖。对于一次性的 code 流程,请参考如何 测试 SMS 验证流程 当该集成是流程的一部分时。

将性能视为度量指标,而非断言

功能测试可以确认列表是否渲染。但它无法可靠地判断重构是否改变了渲染时长或渲染次数,因为测试运行环境会引入噪声。Reassure 会针对特定场景测量这些值,通过重复场景来减少偏差,并在报告有意义的变化前进行统计分析。其 性能测试文档 也涵盖了适用于 CI 和 pull-request 评审的报告功能。

避免使用“渲染必须在指定阈值内完成”这类硬性断言。繁忙的 CI 工作节点可能会导致误报,而过松的阈值则可能漏掉真实的回归问题。应通过多次测量来获取回归信号,并在对比结果出现显著差异时,再针对组件和设备配置进行排查。

测量规则: 功能测试回答的是行为是否正确,而性能工具回答的是测量场景是否发生了变化。请将这两个问题分开处理。

总结与展望

React Native Testing Library 应处于测试策略中快速、广泛的层级。它应覆盖组件行为、可见的状态变化、无障碍体验、校验逻辑、Mock 数据状态以及 JavaScript 组件间的集成。确保这些测试专注于用户可观察到的内容,并让失败信息指向具体的行为,而非庞大的渲染树。

更小规模的基于设备的层级应负责保护那些 Mock 可能失效的业务流。React Native 的 测试概览 指出 RNTL 并不提供完整的 React Native 运行时,因此无法测试原生功能。同样的建议也推荐将组件测试与 Detox 等 E2E 工具结合使用,以覆盖身份验证、支付和核心应用功能等关键流程。

实用的迁移路径

你不需要一次性重写现有的测试套件。

  1. 保留有价值的业务测试。 将纯状态和领域逻辑迁移到专注的单元测试中,以便获得清晰的反馈。
  2. 优先替换实现细节断言。 将 props 和内部状态检查改为可见输出、无障碍状态和交互结果。
  3. 精简快照。 仅保留评审者能够理解并维护的快照。
  4. 补充原生边界覆盖。 对每个重要的 mock 模块,找出仍需验证的设备行为。
  5. 保障关键用户旅程。 为身份验证、支付、核心导航、权限以及其他原生行为可能影响结果的流程补充 E2E 覆盖。
  6. 单独测量敏感页面的性能。 对列表、信息流和高开销渲染路径,使用重复的性能对比,而不是根据 Jest 耗时来猜测。

为了提升发布信心,将测试套件接入 CI/CD 集成测试 并且要求在发布前完成相应测试层的验证。如果 JavaScript 或资源修复在验证后必须触达用户,Capgo 可将已签名的 web bundle 投递到指定 channel,面向 CapacitorJS 和 Electron 应用,并提供发布控制和回滚保护。这种交付流程并不能取代 React Native 设备测试,但它说明了同样的原则:在代码运行的层级验证行为。

关键问题不是 React Native Testing Library 能否测试整个 App。它不能,而官方划定的边界很有用。关键问题是,每个重要风险是否都有在能够暴露它的环境中运行的测试。


Capgo 帮助 CapacitorJS 和 Electron 团队将验证过的 JavaScript 和资源变更接入受控交付,支持定向 channel、发布可见性和回滚保护。请访问 Capgo 了解它如何与你的组件测试、E2E 测试和 CI 测试工作流配合。

为 Capacitor 应用提供实时更新

当 Web 层 Bug 已上线时,通过 Capgo 推送修复,不必等 App Store 审核数天。用户会在后台获取更新,而原生变更仍走常规审核流程。

博客最新文章

Capgo 为你提供打造真正专业移动应用所需的深度洞察。