在 Expo 模块中模拟原生调用
编辑页面
了解如何在 Expo 模块中模拟原生调用。
For the complete documentation index, see llms.txt. Use this Use this file to discover all available pages.
为 Expo 项目编写单元测试的推荐方式是使用 Jest 和 jest-expo 预设。
要为使用原生代码的应用编写单元测试,你需要模拟原生调用。术语 mocking 表示用一个不会执行任何操作的假版本来替换函数的实际实现。对于在本地电脑上运行单元测试,这种方法非常有用,因为它绕过了对原生代码的需求,而原生代码只能在真实的 Android 或 iOS 设备上运行。
Expo SDK 为我们的每个社区包都包含了一组默认的 mock。你也可以使用 Jest 内置 API 自行 mock 任意 JS 代码,例如 mock functions。
不过,为了在你的 Expo 模块中提供默认 mock,我们提供了一种将它们打包的方法。这样可以确保当你的模块使用者运行单元测试时,他们会自动使用 mock 实现。
为模块提供 mock
创建一个与要 mock 的原生模块同名的文件,并将其放入模块的 mocks 目录中。确保从该文件导出 mock 实现。
jest-expo 预设会在运行单元测试时,因为 requireNativeModule 调用而自动返回导出的函数。
例如,expo-clipboard 库有一个名为 ExpoClipboard 的原生模块。你需要在 mocks 目录中创建 ExpoClipboard.ts 来对其进行 mock。
export async function hasStringAsync(): Promise<boolean> { return false; }
现在,在单元测试中调用 ExpoClipboard.hasStringAsync() 会返回 false。
mock 的自动生成
如果原生模块有多个方法,维护原生模块的 mock 可能会非常费工夫。为简化这一点,我们提供了一个脚本,可以自动为模块的 mocks 目录中的所有原生函数生成 mock。它可根据模块中的 Swift 实现生成 TypeScript 和 JavaScript 的 mock。仅存在于 Android 上的方法(例如仅 Kotlin 的 API)不会自动生成。在这些情况下,请在 mocks 目录中手动添加或调整 mock。
要使用此脚本,你需要安装 SourceKitten 框架。然后,进入模块目录(即你的模块的 expo-module.config.json 所在位置),并运行 generate-ts-mocks 命令。
- brew install sourcekitten- npx expo-modules-test-core generate-ts-mocks上面的命令会在你模块的 mocks 目录中生成 ExpoModuleName.ts。它包含了模块中每个原生方法和视图的 mock 实现。
提示: 你也可以运行generate-js-mocks来生成 JavaScript 的 mock。
使用 mock 模块进行单元测试
一旦你为原生模块创建了 mock,就可以编写全面的单元测试,来验证你的 JavaScript 代码是否正确调用了原生函数,并且是否恰当地处理了它们的返回结果。例如,运行 npx expo-modules-test-core generate-ts-mocks 命令会在 example-module/mocks 目录中生成一个类似下面示例的 mock:
/** * 由 expo-modules-test-core 自动生成。 * * 此自动生成文件为原生 Expo 模块提供了一个 mock, * 并且可以直接与 expo jest 预设一起使用。 * */ export type URL = any; export function hello(): any {} export async function setValueAsync(value: string): Promise<any> {} export type ViewProps = { url: URL; onLoad: (event: any) => void; }; export function View(props: ViewProps) {}
以下各节中的示例展示了使用真实测试技术进行全面单元测试的模式,这些技术来自 Expo SDK 模块,例如 expo-clipboard、expo-screen-capture 和 expo-app-integrity。
基本测试设置
在源文件旁边的 tests 目录中创建测试文件。导入你的模块和被 mock 的原生模块来编写断言:
import * as MyModule from '../MyModule'; import ExpoMyModule from '../ExpoMyModule'; describe('MyModule', () => { it('使用正确的参数调用原生模块', async () => { await MyModule.doSomething('test-param'); expect(ExpoMyModule.doSomething).toHaveBeenCalledWith('test-param'); }); });
测试函数调用和返回值
使用 Jest 的 mock 断言方法来验证你的 JavaScript 函数是否正确地将调用委托给了原生实现:
describe('Module functionality', () => { it('委托给原生实现', () => { MyModule.setData('test-data'); expect(ExpoMyModule.setDataAsync).toHaveBeenCalledWith('test-data', {}); }); it('处理异步操作', async () => { await expect(MyModule.getDataAsync()).resolves.not.toThrow(); }); it('验证调用次数', () => { MyModule.performAction(); MyModule.performAction(); expect(ExpoMyModule.performAction).toHaveBeenCalledTimes(2); }); });
使用原生模块测试 React hooks
在测试使用原生模块的 React hooks 时,使用 React Testing Library 的 renderHook 函数:
import { renderHook } from '@testing-library/react-native'; import { useMyHook } from '../useMyHook'; import ExpoMyModule from '../ExpoMyModule'; jest.mock('../ExpoMyModule', () => ({ startOperation: jest.fn().mockResolvedValue(), stopOperation: jest.fn().mockResolvedValue(), })); describe('useMyHook', () => { it('在挂载和卸载时调用原生方法', () => { const hook = renderHook(useMyHook); expect(ExpoMyModule.startOperation).toHaveBeenCalledTimes(1); hook.unmount(); expect(ExpoMyModule.stopOperation).toHaveBeenCalledTimes(1); }); it('处理参数变化', () => { const hook = renderHook(useMyHook, { initialProps: 'param1' }); hook.rerender('param2'); expect(ExpoMyModule.startOperation).toHaveBeenCalledTimes(2); expect(ExpoMyModule.stopOperation).toHaveBeenCalledTimes(1); }); });
最佳实践
- 在测试之间清理:使用
beforeEach或afterEach重置 mock,避免测试污染。 - 测试边界情况:验证原生函数抛出错误或返回意外值时的行为。
- 使用描述性的测试名称:编写能够说明正在验证的具体行为的测试描述。
- 将相关测试分组:使用
describe块按功能或组件组织测试。
更多内容
了解如何设置和配置 jest-expo 包,以便为项目编写单元测试和快照测试。