什么是集成测试?
集成测试被定义为一种将不同部分作为一组进行测试的类型。在我们的案例中,我们要测试的部分是渲染特定区块或编辑器逻辑所需的各个组件。最终,它们与单元测试非常相似,因为它们都使用 Jest 库并通过相同的命令运行。主要区别在于,对于集成测试,我们将使用一个特定的库 react-native-testing-library 来测试编辑器如何渲染不同的组件。
集成测试的结构
一个测试可以由以下部分组成:
- 设置(Setup)
- 渲染(Rendering)
- 查询元素(Query elements)
- 触发事件(Fire events)
- 断言正确的元素行为(Expect correct element behaviour)
- 清理(Cleanup)
我们在以下章节中还包含了常见任务的示例以及提示:
设置(Setup)
这部分通常通过 Jest 回调函数 beforeAll 和 beforeEach 来完成,其目的是准备测试可能需要的一切内容,例如注册区块或模拟部分逻辑。
如果我们期望所有核心区块都可用,这里有一个常见模式的示例:
beforeAll( () => {
// Register all core blocks
registerCoreBlocks();
} );
渲染(Rendering)
在引入测试逻辑之前,我们必须渲染想要测试的组件。根据我们是希望使用作用域组件方法还是整个编辑器方法,这一部分会有所不同。
使用作用域组件方法(Using the Scoped Component approach)
以下是渲染封面区块(Cover block)的一个示例(提取自此代码):
// This import points to the index file of the block
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',
};
...
// Simplified tree to render Cover edit within slot
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 }
/>
);
使用整个编辑器方法(Using the Entire Editor approach)
以下是渲染按钮区块(Buttons block)的一个示例(提取自此代码):
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,
} );
查询元素(Query elements)
一旦组件渲染完成,就该对它们进行查询了。关于这个主题的一个重要说明是,我们应该从用户的角度进行测试,这意味着理想情况下,我们应该通过用户可访问的元素(如文本或无障碍标签)来进行查询。
查询时应遵循以下优先级顺序:
getByText:按文本查询是最接近用户视角的流程,因为文本是他们识别元素的视觉线索。getByLabelText:在某些情况下,我们想查询不提供文本的元素,在这种情况下,我们可以退而求其次使用无障碍标签。getTestId:如果上述选项都不适用,或者我们没有可以依赖的任何可视元素,我们就必须退而求其次使用特定的测试 ID,该 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 查询(Use of find queries)
在渲染组件或触发事件后,由于潜在的状态更新可能会发生副作用,因此我们正在寻找的元素可能尚未渲染。在这种情况下,我们需要等待元素可用,为此,我们可以使用查询函数的 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 queries)
也可以通过 within 函数查询其他元素中包含的元素,这是一个示例:
const missingBlock = await findByLabelText( /Unsupported Block\. Row 1/ );
const translatedTableTitle = within( missingBlock ).getByText( 'Tabla' );
触发事件(Fire events)
与查询元素同样重要的是触发事件以模拟用户交互,为此我们可以使用 fireEvent 函数(文档)。
这是一个按压事件的示例:
按压事件(Press event):
fireEvent.press( settingsButton );
我们还可以触发任何类型的事件,包括自定义事件。在下面的示例中,你可以看到我们如何为 Slider 组件触发 onValueChange 事件(代码引用):
自定义事件 – onValueChange:
fireEvent( heightSlider, 'valueChange', '50' );
断言正确的元素行为(Expect correct element behaviour)
在查询元素并触发事件之后,我们必须验证逻辑是否按预期工作。为此,我们可以使用与单元测试中相同的 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)
最后,我们必须清理任何可能影响后续测试的潜在修改。这是一个典型的清理示例,即在注册区块后进行取消注册所有区块的操作:
afterAll( () => {
// Clean up registered blocks
getBlockTypes().forEach( ( block ) => {
unregisterBlockType( block.name );
} );
} );
辅助函数(Helpers)
为了简化原生版本的集成测试编写,你可以在此 README 中找到一系列辅助函数列表。
常见流程(Common flows)
查询区块(Query a block)
查询区块的一种常见方式是通过其无障碍标签,这是一个示例:
const spacerBlock = await waitFor( () =>
getByLabelText( /Spacer Block\. Row 1/ )
);
有关区块无障碍标签的更多信息,您可以查看 getAccessibleBlockLabel 函数的代码。
添加区块
以下是插入段落(Paragraph)区块的示例:
// Open the inserter menu
fireEvent.press( await findByLabelText( 'Add block' ) );
const blockList = getByTestId( 'InserterUI-Blocks' );
// onScroll event used to force the FlatList to render all items
fireEvent.scroll( blockList, {
nativeEvent: {
contentOffset: { y: 0, x: 0 },
contentSize: { width: 100, height: 100 },
layoutMeasurement: { width: 100, height: 100 },
},
} );
// Insert a Paragraph block
fireEvent.press( await findByText( `Paragraph` ) );
打开区块设置
选中区块后,可以通过点击“Open Settings”按钮来访问区块设置,示例如下:
fireEvent.press( block );
const settingsButton = await findByLabelText( 'Open Settings' );
fireEvent.press( settingsButton );
使用 Scoped Component 方法
当使用 scoped component 方法时,我们需要首先渲染 SlotFillProvider 和 BottomSheetSettings(注意我们要传递 isVisible prop 以强制显示底部工作表),以及该区块本身:
<SlotFillProvider>
<BlockEdit isSelected name={ name } clientId={ 0 } { ...props } />
<BottomSheetSettings isVisible />
</SlotFillProvider>
参见示例:
FlatList 项
FlatList 组件根据其滚动位置、视图和内容大小来渲染其项目。这意味着在渲染此组件时,可能会发生某些项目无法被查询到的情况,因为它们尚未渲染。为了解决这个问题,我们必须显式触发一个事件以使 FlatList 渲染所有项目。
以下是用于在插入器菜单中渲染区块列表的 FlatList 示例:
const blockList = getByTestId( 'InserterUI-Blocks' );
// onScroll event used to force the FlatList to render all items
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 );
工具
使用 Accessibility Inspector
如果您在定位元素标识符时遇到困难,您可能希望使用 Xcode 的 Accessibility Inspector。大多数标识符是跨平台的,因此即使测试默认在 Android 上运行,也可以使用 Accessibility Inspector 找到正确的标识符。

常见陷阱和注意事项
省略 waitFor 函数前的 await 导致误报
省略 waitFor 之前的 await 可能导致测试通过但并未测试预期行为的场景。例如,如果使用 toBeDefined 来断言对 waitFor 调用的结果,断言将通过,因为 waitFor 返回了一个值,尽管它不是我们打算检查的 ReactTestInstance。因此,建议使用自定义匹配器 toBeVisible,它可以防止此类误报。
waitFor 超时
waitFor 函数的默认超时设置为 1000 ms,到目前为止,这个值足以应对我们正在测试的所有渲染逻辑,然而,如果在测试过程中发现某个元素需要更多时间才能渲染,我们应该增加该值。
替换当前的 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;
} );