测试 Expo Router 的配置

了解如何在使用 Expo Router 时为你的应用创建集成测试。


Expo Router 依赖于你的文件系统,这在为集成测试设置模拟时可能会带来挑战。Expo Router 的子模块 expo-router/testing-library 是一组基于流行的 @testing-library/react-native 构建的测试工具,它允许你快速创建预配置用于测试的内存中 Expo Router 应用。

🌐 Expo Router relies on your file system, which can present challenges when setting up mocks for integration tests. Expo Router's submodule, expo-router/testing-library, is a set of testing utilities built on top of the popular @testing-library/react-native and allows you to quickly create in-memory Expo Router apps that are pre-configured for testing.

配置

🌐 Configuration

在继续之前,确保你已经根据你项目中的 使用 Jest 进行单元测试@testing-library/react-native 设置好了 jest-expo

🌐 Before you proceed, ensure you have set up jest-expo according to the Unit testing with Jest and @testing-library/react-native in your project.

注意:在使用 Expo Router 时,不要将测试文件放在 app 目录中。app 目录中的所有文件必须是路由文件或布局文件。相反,应使用 tests 目录或单独的目录。这种方法在 使用 Jest 进行单元测试 中有详细说明。

renderRouter

renderRouter 扩展了 render 的功能,以简化在 Expo Router 中的测试。它返回与 render 相同的查询对象,并且兼容 screen,使你可以使用标准的 query API 来定位组件。

renderRouter 接受与 render 相同的 选项,并引入了一个额外的选项 initialUrl,用于设置模拟深度链接的初始路由。

Inline file system

renderRouter(mock: Record<string, ReactComponent>, options: RenderOptions)

renderRouter 可以通过将一个对象作为第一个参数传递给此函数来提供文件系统的内联模拟。对象的键是模拟文件系统的路径。在定义这些路径时不要使用前导的相对路径(./)或绝对路径(/)表示法,并且排除文件扩展名。

app.test.tsx
import { renderRouter, screen } from 'expo-router/testing-library'; import { View } from 'react-native'; it('my-test', async () => { const MockComponent = jest.fn(() => <View />); renderRouter( { index: MockComponent, 'directory/a': MockComponent, '(group)/b': MockComponent, }, { initialUrl: '/directory/a', } ); expect(screen).toHavePathname('/directory/a'); });

使用 `null` 组件的内联文件系统

renderRouter(mock: string[], options: RenderOptions)

renderRouter 提供一个字符串数组将创建一个带有 null 组件({ default: () => null })的内联模拟文件系统。这对于测试不需要验证路由输出的场景非常有用。

🌐 Providing an array of strings to renderRouter will create an inline mock filesystem with null components ({ default: () => null }). This is useful for testing scenarios where you do not need to test the output of a route.

app.test.tsx
import { renderRouter, screen } from 'expo-router/testing-library'; it('my-test', async () => { renderRouter(['index', 'directory/a', '(group)/b'], { initialUrl: '/directory/a', }); expect(screen).toHavePathname('/directory/a'); });

Path to fixture

renderRouter(fixturePath: string, options: RenderOptions)

renderRouter 可以接受一个目录路径来模拟一个现有的测试夹具。确保提供的路径是相对于当前测试文件的。

app.test.tsx
import { renderRouter } from 'expo-router/testing-library'; import { View } from 'react-native'; it('my-test', async () => { const MockComponent = jest.fn(() => <View />); renderRouter('./my-test-fixture'); });

Path to the fixture with overrides

renderRouter({ appDir: string, overrides: Record<string, ReactComponent>}, options: RenderOptions)

对于更复杂的测试场景,renderRouter 可以同时利用目录路径和内联模拟方法。appDir 参数接受表示目录路径的字符串。overrides 参数是一个内联模拟,可用于覆盖 appDir 中的特定路径。这种组合允许对模拟环境进行精细控制。

🌐 For more intricate testing scenarios, renderRouter can leverage both directory path and inline-mocking methods simultaneously. The appDir parameter takes a string representing a pathname to a directory. The overrides parameter is an inline mock that can be used to override specific paths within the appDir. This combination allows for fine-tuned control over the mock environment.

app.test.tsx
import { renderRouter } from 'expo-router/testing-library'; import { View } from 'react-native'; it('my-test', async () => { const MockAuthLayout = jest.fn(() => <View />); renderRouter({ appDir: './my-test-fixture', overrides: { 'directory/(auth)/_layout': MockAuthLayout, }, }); });

Jest 匹配器

🌐 Jest matchers

以下匹配已添加到 expect,并可用于在 screen 上断言值。

🌐 The following matches have been added to expect and can be used to assert values on screen.

toHavePathname()

将当前的路径名与给定的字符串进行断言。匹配器使用当前 screenusePathname 钩子的值。

🌐 Assert the current pathname against a given string. The matcher uses the value of the usePathname hook on the current screen.

app.test.tsx
expect(screen).toHavePathname('/my-router');

toHavePathnameWithParams()

断言当前的路径名,包括 URL 参数,与给定的字符串相符。这在断言网页浏览器中 URL 的出现时非常有用。

🌐 Assert the current pathname, including URL parameters, against a given string. This is useful to assert the appearance of URL in a web browser.

app.test.tsx
expect(screen).toHavePathnameWithParams('/my-router?hello=world');

toHaveSegments()

将当前段与一组字符串进行断言匹配。匹配器使用当前 screenuseSegments 钩子的值。

🌐 Assert the current segments against an array of strings. The matcher uses the value of the useSegments hook on the current screen.

app.test.tsx
expect(screen).toHaveSegments(['[id]']);

useLocalSearchParams()

将当前本地 URL 参数与一个对象进行断言。匹配器使用当前 screenuseLocalSearchParams 钩子的值。

🌐 Assert the current local URL parameters against an object. The matcher uses the value of the useLocalSearchParams hook on the current screen.

app.test.tsx
expect(screen).useLocalSearchParams({ first: 'abc' });

useGlobalSearchParams()

断言当前屏幕的路径名是否与某个值匹配。使用 useGlobalSearchParams 钩子的值进行比较。

🌐 Assert the current screen's pathname that matches a value. Compares using the value of useGlobalSearchParams hook.

将当前全局 URL 参数与一个对象进行断言。匹配器使用当前 screenuseGlobalSearchParams 钩子的值。

🌐 Assert the current global URL parameters against an object. The matcher uses the value of the useGlobalSearchParams hook on the current screen.

app.test.tsx
expect(screen).useGlobalSearchParams({ first: 'abc' });

toHaveRouterState()

一种高级匹配器,用于针对对象断言当前路由状态。

🌐 An advanced matcher that asserts the current router state against an object.

app.test.tsx
expect(screen).toHaveRouterState({ routes: [{ name: 'index', path: '/' }], });