什么是集成测试?

集成测试被定义为一种将不同部分作为一组进行测试的类型。在我们的案例中,我们要测试的部分是渲染特定区块或编辑器逻辑所需的各个组件。最终,它们与单元测试非常相似,因为它们都使用 Jest 库并通过相同的命令运行。主要区别在于,对于集成测试,我们将使用一个特定的库 react-native-testing-library 来测试编辑器如何渲染不同的组件。

集成测试的结构

一个测试可以由以下部分组成:

我们在以下章节中还包含了常见任务的示例以及提示:

设置(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)

一旦组件渲染完成,就该对它们进行查询了。关于这个主题的一个重要说明是,我们应该从用户的角度进行测试,这意味着理想情况下,我们应该通过用户可访问的元素(如文本或无障碍标签)来进行查询。

查询时应遵循以下优先级顺序:

  1. getByText:按文本查询是最接近用户视角的流程,因为文本是他们识别元素的视觉线索。
  2. getByLabelText:在某些情况下,我们想查询不提供文本的元素,在这种情况下,我们可以退而求其次使用无障碍标签。
  3. 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 找到正确的标识符。

Xcode 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;
} );