在 Expo 模块中模拟原生调用
了解如何在 Expo 模块中模拟原生调用。
为 Expo 项目编写单元测试的推荐方法是使用 Jest 和 jest-expo 预设。
🌐 The recommended way to write unit tests for an Expo project is to use Jest and the jest-expo preset.
要为使用原生代码的应用编写单元测试,你需要模拟原生调用。术语 模拟(mocking) 意味着用一个不会执行任何操作的假的版本来替换函数的实际实现。这种方法对于在本地计算机上运行单元测试非常有用,因为它绕过了只能在实际 Android 或 iOS 设备上运行的原生代码的需求。
🌐 To write a unit test for an app that uses native code, you need to mock native calls. The term mocking means to replace the actual implementation of a function with a fake version that does not perform any actions. This approach is useful for running unit tests on a local computer, as it involves bypassing the need for native code, which can only run on an actual Android or iOS device.
Expo SDK 包含了一套针对我们社区包的默认模拟。你也可以使用内置的 Jest API(例如 模拟函数)自行模拟任何 JS 代码。
🌐 Expo SDK includes a set of default mocks for each of our community packages. You can also mock any JS code yourself using built-in Jest APIs such as mock functions.
然而,为了在你的 Expo 模块中提供默认的模拟,我们提供了一种打包它们的方法。这可以确保当你的模块用户运行单元测试时,他们会自动使用模拟实现。
🌐 However, to provide default mocks in your Expo Module, we offer a method to bundle them. This ensures that when your module user runs unit tests, they will automatically use a mocked implementation.
为模块提供模拟
🌐 Providing mocks for a module
创建一个与你想要模拟的原生模块同名的文件,并将其放置在模块的 mocks 目录中。确保从该文件中导出模拟实现。
jest-expo 预设会在单元测试运行时因为 requireNativeModule 调用而自动返回导出的函数。
🌐 Create a file with the same name as the native module you want to mock and place it in your module's mocks directory. Make sure to export the mock implementation from this file.
The jest-expo preset will automatically return the exported functions because of a requireNativeModule call when running during a unit test.
例如,expo-clipboard 库有一个名为 ExpoClipboard 的原生模块。你将会在 mocks 目录下创建一个 ExpoClipboard.ts 来模拟它。
🌐 For example, the expo-clipboard library has a native module called ExpoClipboard. You will create a ExpoClipboard.ts in the mocks directory to mock it.
export async function hasStringAsync(): Promise<boolean> { return false; }
现在,在单元测试中,调用 ExpoClipboard.hasStringAsync() 会返回 false。
🌐 Now, in a unit test, calling ExpoClipboard.hasStringAsync() returns false.
自动生成模拟
🌐 Automatic generation of mocks
如果本地模块有多个方法,维护其模拟可能会很繁琐。为了简化这一过程,我们提供了一个脚本,可以自动为模块的 mocks 目录中的所有本地函数生成模拟。该脚本可根据模块中的 Swift 实现生成 TypeScript 和 JavaScript 的模拟。仅存在于 Android 上的方法(例如,仅限 Kotlin 的 API)不会自动生成。在这种情况下,需要手动在 mocks 目录中添加或调整模拟。
🌐 Maintaining mocks for native modules can be a lot of work if the native module has multiple methods. To make this easier, we provide a script that automatically generates mocks for all native functions in a module's mocks directory. It works for generating mocks in TypeScript and JavaScript based on the Swift implementation in your module. Methods that exist only on Android (for example, Kotlin-only APIs) will not be generated automatically. In those cases, manually add or adjust the mock in the mocks directory.
要使用此脚本,你必须安装 SourceKitten 框架。然后,导航到模块目录(你的模块的 expo-module.config.json 所在位置)并运行 generate-ts-mocks 命令。
🌐 To use this script, you have to install SourceKitten framework. Then, navigate to the module directory (where your module's expo-module.config.json is located) and run the generate-ts-mocks command.
- brew install sourcekitten- npx expo-modules-test-core generate-ts-mocks上面的命令会在你的模块的 mocks 目录中生成 ExpoModuleName.ts。它包含了你模块中每个原生方法和视图的模拟实现。
🌐 The command above generates ExpoModuleName.ts in the mocks directory of your module. It contains a mock implementation for each native method and view in your module.
信息 提示: 你也可以运行
generate-js-mocks来在 JavaScript 中生成模拟数据。
使用模拟模块进行单元测试
🌐 Unit testing with mocked modules
一旦你为本地模块创建了模拟(mocks),就可以编写全面的单元测试来验证你的 JavaScript 代码是否正确调用本地函数,并适当地处理它们的返回结果。例如,运行 npx expo-modules-test-core generate-ts-mocks 命令,将在 example-module/mocks 目录中生成类似下面示例的模拟文件:
🌐 Once you have created mocks for your native modules, you can write comprehensive unit tests to verify that your JavaScript code calls the native functions correctly and handles their responses appropriately. For example, running npx expo-modules-test-core generate-ts-mocks command, will generate a mock similar to the example shown below inside example-module/mocks directory:
/** * Automatically generated by expo-modules-test-core. * * This autogenerated file provides a mock for native Expo module, * and works out of the box with the expo jest preset. * */ 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)的真实测试技术进行全面单元测试的模式。
🌐 The examples in the following sections demonstrate comprehensive unit testing patterns using real testing techniques from Expo SDK modules such as expo-clipboard, expo-screen-capture, and expo-app-integrity.
基本测试设置
🌐 Basic test setup
在你的源文件旁边的 tests 目录中创建测试文件。导入你的模块和模拟的原生模块来编写断言:
🌐 Create test files in a __tests__ directory next to your source files. Import your module and the mocked native module to write assertions:
import * as MyModule from '../MyModule'; import ExpoMyModule from '../ExpoMyModule'; describe('MyModule', () => { it('calls native module with correct parameters', async () => { await MyModule.doSomething('test-param'); expect(ExpoMyModule.doSomething).toHaveBeenCalledWith('test-param'); }); });
测试函数调用和返回值
🌐 Testing function calls and return values
使用 Jest 的模拟断言方法来验证你的 JavaScript 函数是否正确委托给本地实现:
🌐 Use Jest's mock assertion methods to verify that your JavaScript functions delegate to native implementations correctly:
describe('Module functionality', () => { it('delegates to native implementation', () => { MyModule.setData('test-data'); expect(ExpoMyModule.setDataAsync).toHaveBeenCalledWith('test-data', {}); }); it('handles async operations', async () => { await expect(MyModule.getDataAsync()).resolves.not.toThrow(); }); it('verifies call count', () => { MyModule.performAction(); MyModule.performAction(); expect(ExpoMyModule.performAction).toHaveBeenCalledTimes(2); }); });
使用原生模块测试 React 钩子
🌐 Testing React hooks with native modules
在测试使用原生模块的 React 钩子时,请使用 React Testing Library 的 renderHook 函数:
🌐 When testing React hooks that use native modules, use React Testing Library's renderHook function:
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('calls native methods on mount and unmount', () => { const hook = renderHook(useMyHook); expect(ExpoMyModule.startOperation).toHaveBeenCalledTimes(1); hook.unmount(); expect(ExpoMyModule.stopOperation).toHaveBeenCalledTimes(1); }); it('handles parameter changes', () => { const hook = renderHook(useMyHook, { initialProps: 'param1' }); hook.rerender('param2'); expect(ExpoMyModule.startOperation).toHaveBeenCalledTimes(2); expect(ExpoMyModule.stopOperation).toHaveBeenCalledTimes(1); }); });
最佳实践
🌐 Best practices
- 在测试之间进行清理:使用
beforeEach或afterEach来重置模拟并避免测试污染。 - 测试边缘情况:在原生函数抛出错误或返回意外值时,验证其行为。
- 使用描述性的测试名称:编写测试描述时,要解释正在验证的具体行为。
- 分组相关测试:使用
describe块按功能或组件组织测试。
更多
🌐 More
学习如何设置和配置 jest-expo 包,以便为项目编写单元测试和快照测试。