title: "React Native 集成测试指南" post_status: publish comment_status: open taxonomy: category: - gutenberg-docs post_tag: - React Native - Code - Contributors
React Native 集成测试指南
什么是集成测试?
集成测试被定义为一种将不同部分作为整体进行测试的类型。在我们的场景中,需要测试的部分是特定区块或编辑器逻辑所需渲染的不同组件。最终,它们与单元测试非常相似,因为它们都使用 Jest 库通过相同的命令运行。主要区别在于,对于集成测试,我们将使用特定的库 react-native-testing-library 来测试编辑器如何渲染不同的组件。
Anatomy of an integration test
A test can be structured with the following parts:
We also include examples of common tasks as well as tips in the following sections:
Setup
This part usually is covered by using the Jest callbacks beforeAll and beforeEach, the purpose is to prepare everything that the test might require like registering blocks or mocking parts of the logic.
Here is an example of a common pattern if we expect all core blocks to be available:
beforeAll( () => {
// Register all core blocks
registerCoreBlocks();
} );
渲染
在介绍测试逻辑之前,我们必须先渲染要测试的组件。根据我们是想使用作用域组件方法还是整个编辑器方法,这部分会有所不同。
使用作用域组件方法
以下是渲染封面区块的示例(提取自此代码):
// 此导入指向区块的索引文件
import { metadata, settings, name } from '../index';
...
const setAttributes = jest.fn();
const attributes = {
backgroundType: IMAGE_BACKGROUND_TYPE,
focalPoint: { x: '0.25', y: '0.75' },
hasParallax: false,
overlayColor: { color: '#000000' },
url: 'mock-url',
};
...
// 在插槽内渲染封面编辑器的简化树
const CoverEdit = ( props ) => (
<SlotFillProvider>
<BlockEdit isSelected name={ name } clientId={ 0 } { ...props } />
<BottomSheetSettings isVisible />
</SlotFillProvider>
);
const { getByText, findByText } = render(
<CoverEdit
attributes={ {
...attributes,
url: undefined,
backgroundType: undefined,
} }
setAttributes={ setAttributes }
/>
);
使用完整编辑器方法
以下是渲染 Buttons 块的示例(提取自此代码):
const initialHtml = `<!-- wp:buttons -->
<div class="wp-block-buttons"><!-- wp:button {"style":{"border":{"radius":"5px"}}} -->
<div class="wp-block-button"><a class="wp-block-button__link" style="border-radius:5px" >Hello</a></div>
<!-- /wp:button --></div>
<!-- /wp:buttons -->`;
const { getByLabelText } = initializeEditor( {
initialHtml,
} );
查询元素
组件渲染完成后,接下来就需要查询它们。关于这个话题的一个重要注意事项是:我们应该从用户视角进行测试,这意味着理想情况下应该通过用户可访问的元素(如文本或无障碍标签)进行查询。
查询时应遵循以下优先级顺序:
getByText:从文本查询是最接近用户视角的操作方式,因为文本是用户识别元素的视觉线索。getByLabelText:某些情况下需要查询不提供文本的元素,此时可以回退到使用无障碍标签。getByTestId:如果前两种方式都不适用,且没有任何可依赖的视觉元素,则必须回退到特定的测试ID,可通过testID属性定义(示例参见此处)。
示例如下:
const mediaLibraryButton = getByText( 'WordPress Media Library' );
const missingBlock = getByLabelText( /Unsupported Block\. Row 1/ );
const radiusSlider = getByTestId( 'Slider Border Radius' );
注意:这些查询可以传入纯字符串或正则表达式。正则表达式最适合查询部分字符串(例如:任何包含 Unsupported Block. Row 1 的无障碍标签元素)。需注意特殊字符(如 .)需要进行转义处理。
使用 find 查询
在渲染组件或触发事件后,由于可能的状态更新可能会产生副作用,导致我们正在查找的元素可能尚未渲染。在这种情况下,我们需要等待元素变为可用。为此,我们可以使用查询函数的 find* 版本,这些函数内部使用 waitFor 并定期检查元素是否出现。
以下是一些示例:
const mediaLibraryButton = await findByText( 'WordPress Media Library' );
const missingBlock = await findByLabelText( /Unsupported Block\. Row 1/ );
const radiusSlider = await findByTestId( 'Slider Border Radius' );
在大多数情况下,我们会使用 find* 函数,但需要注意的是,应将其限制在那些确实需要等待元素可用的查询上。
within 查询
也可以通过 within 函数查询其他元素中包含的元素,示例如下:
const missingBlock = await findByLabelText( /Unsupported Block\. Row 1/ );
const translatedTableTitle = within( missingBlock ).getByText( 'Tabla' );
触发事件
与查询元素同等重要的是触发事件以模拟用户交互,为此我们可以使用 fireEvent 函数(文档)。
以下是按压事件的示例:
按压事件:
fireEvent.press( settingsButton );
我们也可以触发任何类型的事件,包括自定义事件。在下面的示例中,你可以看到我们如何为 Slider 组件触发 onValueChange 事件(代码参考):
自定义事件 – onValueChange:
fireEvent( heightSlider, 'valueChange', '50' );
期望正确的元素行为
在查询元素并触发事件后,我们必须验证逻辑是否按预期工作。为此,我们可以使用与单元测试中相同的 Jest expect 函数。建议使用自定义的 toBeVisible 匹配器,以确保元素已定义、是有效的 React 元素且可见。
示例如下:
const translatedTableTitle = within( missingBlock ).getByText( 'Tabla' );
expect( translatedTableTitle ).toBeVisible();
此外,在渲染整个编辑器时,我们还可以验证 HTML 输出是否符合预期:
expect( getEditorHtml() ).toBe(
'<!-- wp:spacer {"height":50} -->\n<div style="height:50px" aria-hidden="true" class="wp-block-spacer"></div>\n<!-- /wp:spacer -->'
);
Cleanup
And finally, we have to clean up any potential modifications we’ve made that could affect the following tests. Here is an example of a typical cleanup after registering blocks that implies unregistering all blocks:
afterAll( () => {
// Clean up registered blocks
getBlockTypes().forEach( ( block ) => {
unregisterBlockType( block.name );
} );
} );
辅助工具
为简化原生版本的集成测试编写,您可以在此 README 中找到一系列辅助函数。
常用流程
查询区块
查询区块的常用方法是使用其无障碍标签,示例如下:
const spacerBlock = await waitFor( () =>
getByLabelText( /Spacer Block\. Row 1/ )
);
如需进一步了解区块的无障碍标签,可查阅 getAccessibleBlockLabel 函数的代码。
添加区块
以下是如何插入段落区块的示例:
// 打开插入器菜单
fireEvent.press( await findByLabelText( 'Add block' ) );
const blockList = getByTestId( 'InserterUI-Blocks' );
// 使用 onScroll 事件强制 FlatList 渲染所有项目
fireEvent.scroll( blockList, {
nativeEvent: {
contentOffset: { y: 0, x: 0 },
contentSize: { width: 100, height: 100 },
layoutMeasurement: { width: 100, height: 100 },
},
} );
// 插入一个段落区块
fireEvent.press( await findByText( `Paragraph` ) );
打开区块设置
选中区块后点击"打开设置"按钮即可访问区块设置,示例如下:
fireEvent.press( block );
const settingsButton = await findByLabelText( 'Open Settings' );
fireEvent.press( settingsButton );
使用作用域组件方法
当使用作用域组件方法时,我们需要先渲染 SlotFillProvider 和 BottomSheetSettings(注意我们传递了 isVisible 属性以强制显示底部面板)以及区块:
<SlotFillProvider>
<BlockEdit isSelected name={ name } clientId={ 0 } { ...props } />
<BottomSheetSettings isVisible />
</SlotFillProvider>
参考示例:
FlatList 项目
FlatList 组件会根据滚动位置、视图和内容尺寸来渲染其项目。这意味着在渲染此组件时,可能会出现某些项目因尚未渲染而无法被查询的情况。为了解决这个问题,我们必须显式触发一个事件,使 FlatList 渲染所有项目。
以下是在插入器菜单中用于渲染区块列表的 FlatList 示例:
const blockList = getByTestId( 'InserterUI-Blocks' );
// onScroll 事件用于强制 FlatList 渲染所有项目
fireEvent.scroll( blockList, {
nativeEvent: {
contentOffset: { y: 0, x: 0 },
contentSize: { width: 100, height: 100 },
layoutMeasurement: { width: 100, height: 100 },
},
} );
滑块
底部工作表中的滑块应使用其 testID 进行查询:
const radiusSlider = await findByTestId( 'Slider Border Radius' );
fireEvent( radiusSlider, 'valueChange', '30' );
请注意,滑块的 testID 是 "Slider " + 标签。因此,对于标签为 "Border Radius" 的滑块,其 testID 为 "Slider Border Radius"。
选择内部区块
添加区块时需要注意:如果区块包含内部区块,这些内部区块默认不会渲染。以下示例展示了如何让 Buttons 区块渲染其内部的 Button 区块(假设我们已经获取到 Buttons 区块的引用 buttonsBlock):
const innerBlockListWrapper = await within( buttonsBlock ).findByTestId(
'block-list-wrapper'
);
fireEvent( innerBlockListWrapper, 'layout', {
nativeEvent: {
layout: {
width: 100,
},
},
} );
const buttonInnerBlock = await within( buttonsBlock ).findByLabelText(
/Button Block\. Row 1/
);
fireEvent.press( buttonInnerBlock );
Tools
Using the Accessibility Inspector
If you have trouble locating an element’s identifier, you may wish to use Xcode’s Accessibility Inspector. Most identifiers are cross-platform, so even though the tests are run on Android by default, the Accessibility Inspector can be used to find the right identifier.

常见陷阱与注意事项
省略 waitFor 函数前 await 导致的误判
在 waitFor 前省略 await 可能导致测试通过但未验证预期行为的情况。例如,若使用 toBeDefined 断言 waitFor 调用结果,由于 waitFor 本身会返回值,断言将通过——即便该值并非我们预期验证的 ReactTestInstance。因此,建议使用自定义匹配器 toBeVisible 来防范此类误判。
waitFor 超时设置
waitFor 函数的默认超时时间设置为 1000 毫秒,目前该值足以满足我们测试的所有渲染逻辑。然而,如果在测试过程中发现某个元素需要更长的渲染时间,则应适当增加此值。
替换现有 UI 单元测试
部分组件已具备覆盖组件渲染的单元测试,虽然并非强制要求,但在这些情况下,建议分析迁移至集成测试的可行性。
若需同时保留两种测试,我们将在集成测试文件名中添加“integration”一词以避免命名冲突,示例如下:packages/block-library/src/missing/test/edit-integration.native.js。
平台选择
默认情况下,Jest 中的所有测试都使用 Android 平台运行。因此,如果我们需要测试与不同平台相关的特定行为,就需要支持平台测试文件。
如果我们只需要测试由 Platform 对象控制的逻辑,可以使用以下代码模拟该模块(此示例将平台更改为 iOS):
jest.mock( 'Platform', () => {
const Platform = jest.requireActual( 'Platform' );
Platform.OS = 'ios';
Platform.select = jest.fn().mockImplementation( ( select ) => {
const value = select[ Platform.OS ];
return ! value ? select.default : value;
} );
return Platform;
} );