Ignite React Native 项目测试指南:Jest 单元测试、Mock 与 Maestro 集成测试实战
【免费下载链接】igniteInfinite Red's battle-tested React Native project boilerplate, along with a CLI, component/model generators, and more! 9 years of continuous development and counting.项目地址: https://gitcode.com/GitHub_Trending/ig/ignite
导读:本文以 Infinite Red 的 Ignite React Native 项目样板(boilerplate)为背景,系统讲解其官方测试方法论——"Write tests. Not too many. Mostly integration."(写测试,不要太多,主要是集成测试)。你将掌握:Ignite 中 Jest 单元测试的编写结构、Given/When/Then 最佳实践、
pnpm run test与pnpm run test:watch的用法、何时该为哪些代码写单测,以及jest.fn、jest.mock等 Mock 策略;同时了解基于 Maestro 的端到端(E2E)集成测试与 Ignite 自带测试实例的源码级剖析。
测试哲学:为什么是"Not too many. Mostly integration."
Infinite Red 在开发 Ignite 时秉持一个朴素而务实的理念:对交付的代码要有信心,确信它不会破坏用户的体验。这个理念被浓缩成一句来自 Guillermo Rauch 的名言:
Write tests. Not too many. Mostly integration. (写测试。不要太多。主要是集成测试。)
这句话不是一条死板的规则,但它非常准确地表达了 Ignite 的测试策略倾向:
- 写测试:测试是工程质量的一部分,不是可选项;
- 不要太多:并非每行代码都值得写单测,过度追求覆盖率反而会拖累开发效率;
- 主要是集成测试:相比孤立的单元测试,Ignite 更重视端到端的集成验证(例如用 Maestro 模拟真实用户操作流程),因为这类测试最能反映"用户实际体验是否被破坏"。
在这一哲学之下,Ignite 的测试体系主要分为两条主线:Maestro 集成测试(E2E)与Jest 单元测试。仓库中 boilerplate/package.json 的 scripts 也印证了这一分工:test(Jest 单测)、test:watch(Jest 监听模式)、test:maestro(Maestro E2E)。
Maestro 集成测试:以真实用户视角验证完整流程
Maestro 是 Ignite 首选的集成测试工具,它通过 YAML 描述"用户操作流程",在真实 App 上执行点击、输入、断言等动作,验证从启动到关键页面的完整链路是否可用。
Ignite 样板在 boilerplate/.maestro 目录中预置了开箱即用的 Maestro 流程:
- boilerplate/.maestro/flows/Login.yaml:主流程,使用默认凭据登录并进入演示页面;
- boilerplate/.maestro/flows/FavoritePodcast.yaml:播客收藏流程;
- boilerplate/.maestro/shared/_Login.yaml 与
_OnFlowStart.yaml:可复用的共享子流程。
以登录流程为例,其 YAML 结构清晰展示了 Maestro 的"断言 + 动作"模式:
#flow: Shared _Login appId: ${MAESTRO_APP_ID} --- - assertVisible: "Log In" - tapOn: text: "Tap to Log in!" - assertVisible: "Your app, almost ready for launch!" - tapOn: text: "Let's go!" - assertVisible: "Components to jump start your project!"几个关键点:
appId使用${MAESTRO_APP_ID}环境变量注入,运行测试时通过-e参数传入。Ignite 的 npm 脚本已封装好:pnpm run test:maestro对应 boilerplate/package.json 中的
maestro test -e MAESTRO_APP_ID=com.helloworld .maestro/flows。runFlow支持流程复用,onFlowStart可声明每个流程启动前都要执行的前置步骤,这正是共享子流程的设计初衷。- 断言文本直接对应界面上的真实文案,因此 Maestro 测试天然与 i18n 文案耦合——一旦改动了关键界面文案,对应的 flow 也需要同步更新。
关于 Maestro 的完整安装与入门步骤,官方提供了 Ignite Cookbook 配方文档,此处不再展开;你可以在本地基于已生成的 Ignite 项目(
ignite new之后)直接运行上述预置 flows 体验效果。
单元测试:Jest 与测试结构
单元测试覆盖代码中最小粒度的部分,比如单个函数或类。
—— React Native 官方文档《Testing》对单元测试的定义
在 Ignite 中,单元测试主要服务于纯工具函数。测试运行器是Jest,测试文件用it或test语句声明——第一个参数是对测试行为的描述,第二个参数是执行测试代码的函数:
it("given a date in the past, colorForDueDate() returns red", () => { const input = colorForDueDate("2000-10-20") expect(input).toBe("red") })在测试函数内部,通过expect函数做断言:把被测值作为第一个参数传入expect,再调用匹配器方法(matcher,如.toBe)描述期望值。it、test、expect等 Jest 全局函数由 Jest 测试运行器自动加载,无需显式 import。
测试命名与结构的最佳实践:Given / When / Then(AAA)
编写测试时,一个好的测试应当包含以下三段信息:
- Given—— 某个前置条件(precondition);
- When—— 被测函数执行的某个动作;
- Then—— 期望的最终结果。
这就是业界常说的AAA(Arrange, Act, Assert)模式。Ignite 官方推荐的测试描述风格,正是把这三要素直接写进测试名称中,例如上例的"given a date in the past, colorForDueDate() returns red"——given前置条件 +When的动作(传入日期)+Then的结果(返回红色)。
如何编写并运行单元测试
编写:在app或test目录下创建以.test.ts结尾的文件即可(例如app/utils/formatDate.test.ts)。
运行全部单测:
pnpm run test这会用 Jest 执行全部单元测试。
迭代开发模式(监听):
pnpm run test:watchtest:watch启动一个常驻的 Jest 进程,每次保存文件都会自动重跑相关测试。在调试逻辑、微调取值时,它能提供近乎即时的反馈,是开发体验提升的关键工具。
何时该写单元测试
写测试前最该问的问题是:"什么代码值得单元测试?"并非每行代码都能从单测中获益。通常,无需外部依赖(如 API)、且包含非平凡逻辑的代码最适合单测。Ignite 官方给出了三类典型场景:
- 复杂的正则表达式:正则往往只有在编写的那一刻才"读得懂"。用一组合法/非法输入去测试,能确保未来接手的人(包括几个月后的自己)不会改坏它。
- 嵌套的 if/else 语句:当一个函数重度依赖大量条件分支时,靠人肉遍历所有分支几乎不可能。测试能帮助你确保每条分支都被覆盖到。
- 校验类函数:比如
isJson()这类"验证值是否符合特定形状"的函数。当关键业务代码的正确性依赖它时,必须为它写测试兜底。
Mocking:让被隔离的代码"有依赖可依"
现实中的代码很少是纯粹的函数。多数 App 存在副作用——发起网络请求、调用原生模块、访问全局对象等。
- 在集成测试中,可以通过搭建合适的测试环境来处理这些副作用;
- 在单元测试中,我们追求"隔离被测代码",更常见的做法是为外部依赖提供Mock(模拟)。
Jest 提供了多层次的 Mock 策略,Ignite 文档逐一给出了用法与示例。
Mock Functions:模拟回调函数
可以用jest.fn创建一个模拟回调函数:
// 接收输入值,加上 42 const mockCallback = jest.fn((x) => 42 + x)它调用起来与普通函数无异:
const added = [0, 1].map(mockCallback)但它的身上额外挂载了.mock等属性,供测试中后续断言:
// 该 mock 函数被调用了两次,数组里每项一次 expect(mockCallback.mock.calls.length).toBe(2).mock属性记录了调用次数、调用参数、返回值与this上下文等全部元数据,是断言"函数如何被调用"的核心依据。
Mock Modules:模拟模块
测试依赖axios这类网络库的代码很棘手——返回值受真实网络影响,不可控。解决方案是把整个库 Mock 掉,返回静态数据:
import { View, Text } from "react-native" import axios from "axios" export const getUsers = () => axios.get("/users").then((res) => res.data)对应的测试:
import axios from "axios" import { getUsers } from "./users" jest.mock("axios") test("should fetch users", () => { const users = [{ name: "Bob" }] const res = { data: users } axios.get.mockImplementation(() => Promise.resolve(res)) getUsers().then((data) => { expect(data).toEqual(users) }) })这里jest.mock("axios")自动把axios替换为 mock 版本,mockImplementation则指定axios.get的返回实现,从而让每次测试都能拿到确定的数据。
Mock React Native 原生模块
除了普通 JS 库,React Native 的原生模块同样可以 Mock。语法如下:
jest.mock('react-native-video', () => 'Video');jest.mock的第一个参数是要 Mock 的模块名;第二个参数可选,是一个返回模块的工厂函数。上面的例子中工厂函数直接返回字符串'Video',等价于让该模块导出一个默认导出。
Ignite 测试基础设施源码剖析
理解文档理论后,再回看 Ignite 仓库中的实际测试代码,能更直观地掌握"在真实项目里该怎么做"。这些文件同时也可以作为你动手写测试的参照模板。
1. Jest 配置:极简而关键
Ignite 的 Jest 配置非常精简(boilerplate/jest.config.js):
module.exports = { preset: "jest-expo", setupFiles: ["<rootDir>/test/setup.ts"], }preset: "jest-expo":使用 Expo 官方 Jest 预设,自动处理 React Native / Expo 的 Babel 转译与模块解析;setupFiles:在测试运行前加载 boilerplate/test/setup.ts,完成全局 Mock 的初始化。
配合 boilerplate/package.json 的 devDependencies,可以看到完整测试技术栈:jest、jest-expo、@testing-library/react-native、react-test-renderer、ts-jest、@types/jest、babel-jest。这印证了"Jest 单测 + Testing Library 组件测试"的双层体系。
2. 全局 Mock:test/setup.ts
boilerplate/test/setup.ts 是 Ignite 测试基建的心脏,它在每个测试文件运行前统一 Mock 掉那些"在 Node 环境里不可用或不可控"的依赖:
react-native的Image:Mock 掉resolveAssetSource(返回 mockFile)与getSize(固定回调success(100, 100)),避免测试中加载真实图片资源;i18next:Mock 成t/translate直接返回key + JSON.stringify(params)的简单实现;expo-localization:固定返回en-US区域设置;app/i18n:Mock 掉 i18n 初始化对象,避免测试触发真实的国际化加载流程。
这一文件是"Mock React Native 模块"策略在生产级项目中的完整范例——新建测试文件时,你几乎不需要再为这些通用依赖重复写 Mock。
3. 纯函数单测范例:apiProblem.test.ts
boilerplate/app/services/api/apiProblem.test.ts 是对纯函数getGeneralApiProblem的 9 个测试用例,完美呼应文档中"纯函数最值得单测"的观点。它把 apisauce 的错误响应对象映射为业务层友好的结果:
test("handles connection errors", () => { expect(getGeneralApiProblem({ problem: "CONNECTION_ERROR" } as ApiErrorResponse<null>)).toEqual({ kind: "cannot-connect", temporary: true, }) }) test("handles unauthorized errors", () => { expect( getGeneralApiProblem({ problem: "CLIENT_ERROR", status: 401 } as ApiErrorResponse<null>), ).toEqual({ kind: "unauthorized", }) })注意这里的断言风格:连接错误/网络错误/超时/未知错误映射为带temporary: true的对象,401/403/404 分别映射为unauthorized/forbidden/not-found,而CANCEL_ERROR返回null。这正是"校验函数"类代码单测的典型写法——输入可控、输出确定、边界齐全。你可以在app目录下创建同名.test.ts文件仿照此模式为其他纯工具函数补测试。
4. 依赖副作用单测范例:storage.test.ts
boilerplate/app/utils/storage/storage.test.ts 展示了如何测试"依赖原生模块(MMKV)"的工具函数。它使用describe分组 +beforeEach重置环境:
describe("MMKV Storage", () => { beforeEach(() => { storage.clearAll() storage.set("string", "string") storage.set("object", JSON.stringify(VALUE_OBJECT)) }) it("should save objects", () => { save("object", { y: 2 }) expect(load<object>("object")).toEqual({ y: 2 }) }) it("should clear all data", () => { expect(storage.getAllKeys()).toEqual(["string", "object"]) clear() expect(storage.getAllKeys()).toEqual([]) }) })其要点在于:利用beforeEach让每个用例从相同初始状态出发,保证测试相互独立、可重复执行——这是"Given 前置条件"最佳实践在真实代码中的应用。
5. 组件测试范例:Text.test.tsx
对于 React 组件,Ignite 使用React Native Testing Library(@testing-library/react-native,它是@testing-library/react在 RN 上的移植)渲染组件并断言输出:
import { render } from "@testing-library/react-native" it("should render the component", () => { const { getByText } = render( <ThemeProvider> <NavigationContainer> <Text text={testText} /> </NavigationContainer> </ThemeProvider>, ) expect(getByText(testText)).toBeDefined() })完整代码见 boilerplate/app/components/Text.test.tsx。这个示例说明:组件测试的重点是"渲染后用户能看到什么"(getByText),而不是内部实现细节。值得注意的是,被测组件依赖 Theme 与 Navigation 上下文,因此测试时要用ThemeProvider、NavigationContainer包裹——这正是集成测试思维在组件层面的体现。
6. 全局健壮性测试范例:i18n.test.ts
boilerplate/test/i18n.test.ts 是 Ignite 内置的一个非常实用的"防呆"测试:它通过grep扫描整个app目录,找出所有tx="..."属性与translate("...")调用中引用的 i18n key,再与 boilerplate/app/i18n/en.ts 中定义的翻译 key 比对,确保没有漏定义或拼写错误的翻译键,避免运行时渲染出空字符串。
它的局限也很明确(源码注释中已说明):如果 key 被存在变量里再条件性地使用,就不会被 grep 捕捉到;这类 key 需要手动加入文件顶部的EXCEPTIONS数组豁免。这与 docs/boilerplate/test/Test.md 中对test目录的说明一致——Jest 测试可以放在代码库任何位置,但全局性的 Jest 初始化、Mock 与全局作用域测试统一归入test目录。
测试资源与选型参考
如果要在 Ignite App 中扩展测试能力,以下是官方推荐的生态方向:
- React Native Testing Library:
@testing-library/react的 RN 移植版,最适合做组件单元测试(Ignite 已将其预置在 devDependencies 中); - Detox 与 Appium:Maestro 之外的两款集成测试替代方案,可按团队习惯与 CI 环境选择;
- Jest 官方 React Native 教程:涵盖 Mock 原生模块、快照测试等进阶用法;
- Maestro 官方"Why Maestro"文档:阐述其设计动机与使用场景;
- Kent C. Dodds 的测试文章:关于"写多少测试、测什么"的深入思考,与 Ignite 的测试哲学一脉相承。
小结
回到开篇那句"Write tests. Not too many. Mostly integration.",Ignite 的测试体系可以概括为一条清晰的实践路径:
- 以 Maestro 覆盖核心用户流程(登录、收藏等),守住体验底线;
- 以 Jest +
pnpm run test守护纯函数与工具逻辑,用test:watch在开发中保持快速反馈; - 用
jest.fn/jest.mock隔离副作用,借助 boilerplate/test/setup.ts 的全局 Mock 免去重复劳动; - 参照仓库内四个测试实例(apiProblem / storage / Text / i18n)建立自己的测试模板,覆盖纯函数、依赖副作用、组件渲染与全局资源四类场景。
无论你是刚用ignite new创建项目,还是在维护老应用,这套方法论都能直接迁移到你的日常开发中——先写"给用户看的测试"(Maestro),再写"给逻辑兜底的测试"(Jest),而不要陷入"为覆盖率而测试"的陷阱。
【免费下载链接】igniteInfinite Red's battle-tested React Native project boilerplate, along with a CLI, component/model generators, and more! 9 years of continuous development and counting.项目地址: https://gitcode.com/GitHub_Trending/ig/ignite
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考