Gutenberg 同时包含 PHP 和 JavaScript 代码,并鼓励对两者都进行测试和代码风格检查。

为什么要测试?

除了测试会给你的生活带来乐趣之外,测试之所以重要,不仅因为它们有助于确保我们的应用程序按预期运行,还因为它们提供了如何使用某段代码的简洁示例。

测试也是我们代码库的一部分,这意味着我们对测试采用与所有应用程序代码相同的标准。

与所有代码一样,测试也需要维护。为了写测试而写测试并不是目标——相反,我们应该努力在覆盖预期与非预期行为、快速执行和代码维护之间取得恰当的平衡。

在编写测试时,请考虑以下几点:

  • 我们正在测试的是什么行为?
  • 运行这段代码时可能发生哪些错误?
  • 该测试是否真的在测试我们认为它所测试的内容?还是说我们正在引入误报/漏报?
  • 它是否易于阅读?其他贡献者能否通过查看代码对应的测试来理解我们代码的行为?

JavaScript 测试

JavaScript 单元测试和集成测试使用 Vitest。请从 vitest 中显式导入 describe、test、expect、钩子函数以及 vi。全局变量已被禁用。Gutenberg 的共享 setup 属于内部实现;外部项目应遵循消费者迁移指南。

设置与命令

先从开发先决条件开始,然后在仓库根目录下运行以下命令:

npm ci
npm run build
npm exec --no --workspace @wordpress/unit-tests -- playwright install chromium

完整测试套件需要 Chromium,因为它包含浏览器模式(Browser Mode)测试。JavaScript 单元测试不需要本地 WordPress 站点。

# 先进行 lint 检查,然后完整运行一次 Vitest 测试套件。
npm test

# 不进行 lint 检查,完整运行一次测试套件。
npm run test:unit

# 聚焦某个文件或目录,并可选择按测试名称进行过滤。
npm run test:unit -- packages/escape-html/src/test/index.ts
npm run test:unit -- packages/escape-html/src/test/index.ts -t escapeAttribute

# 监视聚焦的一组测试,并在编辑后重新运行。按 q 退出。
npm run test:unit:watch -- packages/escape-html/src/test/index.ts

# 为匹配的测试更新快照,然后查看生成的 diff。
npm run test:unit:update -- path/to/tests

# 暂停 Node 或 jsdom 测试 worker,直到调试器连接。
npm run test:unit:debug -- packages/escape-html/src/test/index.ts

路径用于过滤所发现的文件;-t 用于过滤测试名称。使用 --project=node、--project=jsdom 或 --project=browser 来选择环境。这些过滤器不会改变由文件名确定的环境。例如,npm run test:unit -- --project=browser 会运行 Browser 测试套件。

npm run test:unit:vitest 及其 watch/update 变体仍然是兼容的别名。Gutenberg 自有的测试仅通过 Vitest 运行。仍保留 Jest 的外部项目可以使用公开的 wp-scripts test-unit-jest 适配器,并遵循消费者迁移指南。

请独立运行 npm run lint 以进行代码风格检查。ESLint 用于强制执行 JavaScript 规则;npm run typecheck 会检查 TypeScript 以及参与类型检查的 JavaScript(包括测试和 story)。npm run build 会生成声明文件,但不会进行类型检查。运行 npm run test:unit:routing 和 npm run test:unit:conventions 可检查测试发现机制、导入以及环境约定。请配置编辑器 lint 集成,以便在编辑时获得反馈。

路由和基础设施检查

npm run test:unit:routing 会将 Vitest 实时发现的测试与仓库的测试文件模式进行比对。每个被发现的测试都必须恰好属于一个项目,该项目由其文件名决定。该检查会拒绝缺失的测试、重复的归属、逐文件的环境覆盖以及过时的 Jest 运行器基础设施。它不依赖固定的测试数量或迁移元数据。必需的 All CI 检查会运行它。

npm run test:unit:conventions 会检查显式的 Vitest 导入、workspace 依赖、TypeScript 测试图、环境约定以及隔离默认设置。在更改测试基础设施时,请保留现有的 Node 24/26 矩阵、每个运行时四个 Node/jsdom 分片、Node 24 上的一个 Chromium 作业、时区检查以及 Storybook 冒烟测试覆盖。

npm run test:unit:vitest:shuffled 会打乱文件以及每个文件内部测试的顺序。CI 在所有 Node/jsdom 分片和 Chromium 作业中使用 GITHUB_RUN_NUMBER 作为种子。每次新的工作流运行都会使用不同的种子;重新运行该工作流则保持相同的种子。每个作业都会记录其种子和一条复现命令。在复现失败时,请使用相同的 checkout 和 Node.js 版本。

在本地,Vitest 会从当前时间选取种子,除非你传递了 --sequence.seed。提供 CI 日志中的种子即可复现其排序,并保留日志中显示的 project 和 shard 参数。例如:

npm run test:unit:vitest:shuffled -- --sequence.seed=12345 --project=node --project=jsdom --shard=1/4
npm run test:unit:vitest:shuffled -- --sequence.seed=12345 --project=browser

新的种子可能会在无关的拉取请求上暴露已存在的测试顺序依赖。发生这种情况时,请使用记录的种子复现失败,并修复共享状态或缺失的 setup 与清理。不要为了获得不同的种子而推送无关的更改。如果无法及时修复,请连同种子、commit、Node.js 版本、项目和分片一起报告失败,并与维护者商定一个临时隔离(quarantine)方案。隔离应仅限于受影响的测试,并为修复以及移除隔离关联一个跟踪 issue。重新运行最初失败的种子以验证修复。

公开工具的消费者

运行 npm run test:unit:consumers 可打包公开的工具、在 workspace 之外安装它,并实际演练文档中记载的配置和命令。该检查独立于 Gutenberg 的单元测试运行器,涵盖 Vitest 和仅用于维护的 Jest 适配器。请保留这些隔离消费者所安装的依赖,包括已发布的 WordPress Jest 预设和 Babel 转换器。

  • --vite=<version> 选择一个受支持的 Vite 版本。
  • --browser 包含 Chromium 和生成的 CSS 覆盖率。
  • --node=/absolute/path/to/node 选择消费者运行时。
  • --lockfile=/path/to/package-lock.json 会在重新打包已更改的包时重放早前的消费者依赖解析。同时还应验证全新安装,以确保锁文件不会隐藏 peer 依赖冲突。
  • --scripts=<published-version> 和 --eslint-plugin=<published-version> 用于验证注册表发布。基于打包源码的检查并不能替代这种发布验证。

该检查会报告实际的 Node、Vite、Vitest 和构建版本。请验证受支持的 Node/Vite 组合,并随工具变更一起记录已发布的版本和结果。软件包发布指南介绍了受保护的发布流程。剩余的迁移发布和 npm 弃用关卡在 #80855 中跟踪。

文件夹结构

将测试保存在你工作目录下的 test 文件夹中。测试文件应与被测试的目标文件同名。

对于 DOM 结构、语义、事件、状态以及其他不依赖浏览器渲染的确定性行为,请使用 *.jsdom.test.*。对于生成的样式和计算样式、层叠、响应式行为、布局、几何、渲染后的可见性、动画、滚动、原生可聚焦性和 Tab 顺序、指针行为、插入符(caret)几何、ResizeObserver 以及媒体查询,请使用 *.browser.test.*。

在直接的 React 浏览器模式测试中,请从 vitest-browser-react 导入并 await render 或 renderHook。从 vitest/browser 导入 userEvent,并在处理异步浏览器状态时优先使用定位器(locator)。当浏览器模式没有等效功能时,可以保留 Testing Library 的查询辅助函数,但请使用浏览器模式的 React 渲染器。共享的 initializeEditor 集成辅助函数是现有的渲染器例外。

当所提供的矩形、观察者通知和定时器是算法或生命周期测试的有意输入时,它们可以保留在 jsdom 中。仅在 setup 中使用浏览器 API 并不足以表明断言需要浏览器模式。请让例外保持具体明确,并在不再需要时将其移除。

浏览器模式仅加载由测试图或其 setup 导入的 CSS。当断言依赖于 WordPress 通常单独 enqueue 的样式时,请显式导入该包的全局 Sass。请将确定性的浏览器 API 模拟限制在非可视化测试的局部范围内,并在之后恢复它们。

保持 Node 兼容的测试名称不带环境后缀。每个新测试都会自动在 Vitest 中运行。文件名决定其运行环境。请勿使用针对单个文件的环境覆盖设置。

+-- test
|   +-- bar.test.js
+-- bar.js

只有测试文件(至少包含一个测试用例)才应直接放在 /test 目录下。如果需要添加外部 mock 或 fixture,请将它们放在子文件夹中,例如:

  • test/mocks/[file-name].js
  • test/fixtures/[file-name].js

导入测试

鉴于前述文件夹结构,在导入你要测试的代码时,请尽量使用相对路径,而非项目路径。

推荐写法:

import { bar } from '../bar';

避免这种写法:

import { bar } from 'components/foo/bar';

如果日后你决定将代码移动到应用程序目录中的其他位置,这样做会让你的工作轻松得多。

描述测试

使用 describe 代码块对测试用例进行分组。理想情况下,每个测试用例应只描述一种行为。

在测试用例中,请尽量用平实的语言描述预期行为。对于 UI 组件,这可能意味着要从用户视角描述预期行为,而不是解释代码的内部实现。

推荐写法:

import { describe, test } from 'vitest';

describe( 'CheckboxWithLabel', () => {
    test( 'checking checkbox should disable the form submit button', () => {
        ...
    } );
} );

避免这种写法:

import { describe, test } from 'vitest';

describe( 'CheckboxWithLabel', () => {
    test( 'checking checkbox should set this.state.disableButton to `true`', () => {
        ...
    } );
} );

设置和清理方法

Vitest 提供了设置和清理方法,允许你在每个或全部测试、或特定 describe 代码块内的测试运行之前和之后执行任务。

这些方法可以处理异步代码,从而允许你完成通常无法以内联方式进行的设置。与异步测试一样,返回一个 Promise 或使用 async/await。Vitest 会等待其完成:

import { afterAll, beforeAll } from 'vitest';

// 为*所有*测试进行一次性设置
beforeAll( () =>
    someAsyncAction().then( ( resp ) => {
        window.someGlobal = resp;
    } )
);

// 为*所有*测试进行一次性清理
afterAll( () => {
    window.someGlobal = null;
} );

afterEach 和 afterAll 为我们在测试之后进行“清理”提供了一种完美的(也是首选的)方式,例如重置状态数据。

请避免将清理代码放在断言之后,因为如果其中任何一个测试失败,清理将不会执行,进而可能导致无关测试失败。

Vitest 会在测试之间重置 mock 实现和调用历史、恢复 spy、重置被 stub 的全局变量和环境变量,并恢复真实计时器。请在每个测试的 setup 钩子中配置所需的 mock 实现。导入的模块状态不会自动重置;需要全新的模块实例时,请显式重置它或使用 vi.resetModules()。请勿禁用模块隔离,也不要启用全局 Vitest API。

wpVitest 是一项需显式启用的功能,供需要在 vi.hoisted() 中使用提升安全(hoist-safe)辅助函数的 jsdom 测试套件使用。

预期的控制台调用

Gutenberg 的内部 Vitest 配置会在每个测试之前清除 @wordpress/deprecated 的已记录消息历史。在单个测试内,一条消息仍然只会记录一次。测试套件无需自行清理弃用历史。要在行为测试中屏蔽已知的弃用警告,请在 beforeEach 中设置其 logged 条目,并在断言该警告的测试中删除该条目。

当 console.error、console.warn、console.info 或 console.log 存在测试未显式预期的调用时,Gutenberg 的内部 Vitest 配置会使该测试失败。请使用带具体参数的 toHaveErroredWith、toHaveWarnedWith、toHaveInformedWith 或 toHaveLoggedWith。也支持诸如 expect.objectContaining 这类非对称匹配器。

expect( console ).toHaveWarnedWith( 'The setting is deprecated.' );

成功的正向断言会计入 mock 历史中所有已有的匹配调用(包括重复调用)。它不会改动历史记录,因此重复断言和单独的调用次数检查仍然有效。其他调用(包括之后带有相同参数的调用)需要各自的断言。反向断言和失败的断言不会计入任何调用。

诸如 toHaveWarned() 这类无参数匹配器出于兼容性考虑仍受支持,并会计入历史中对该方法的所有调用。因此,宽泛的断言仍可能掩盖无关调用。请优先使用具体参数,以便只计入匹配的调用。标准的 spy 断言(如 toHaveBeenCalledWith)在该辅助工具中不计入控制台调用。

该检查会在测试及其清理钩子之后运行,因此支持在 afterEach 中进行断言。清除、重置或恢复 mock 并不能让未计入的调用免于检查。请在清除历史之前对预期调用进行断言。每个测试都会从头开始计入,检查失败之后也是如此。

Mock 依赖

依赖注入

将依赖作为参数传递给函数,通常能让你的代码更易于测试。请尽可能避免在更高的作用域中引用依赖。

避免这种写法:

import VALID_VALUES_LIST from './constants';

function isValueValid( value ) {
    return VALID_VALUES_LIST.includes( value );
}

在这里,为了让测试通过,我们不得不导入并使用 VALID_VALUES_LIST 中的某个值:

expect( isValueValid( VALID_VALUES_LIST[ 0 ] ) ).toBe( true );

上面的断言测试了两个行为:1) 函数能够检测列表中的项;2) 函数能够检测 VALID_VALUES_LIST 中的项。

但如果我们并不关心 VALID_VALUES_LIST 中存储的内容,或者该列表是通过 HTTP 请求获取的,而我们只想测试 isValueValid 能否检测出列表中的项,又该怎么办呢?

推荐写法:

function isValueValid( value, validValuesList = [] ) {
    return validValuesList.includes( value );
}

由于我们将列表作为参数传递,因此可以在测试中传入 mock 的 validValuesList 值,而且还能额外测试更多场景:

expect( isValueValid( 'hulk', [ 'batman', 'superman' ] ) ).toBe( false );

expect( isValueValid( 'hulk', null ) ).toBe( false );

expect( isValueValid( 'hulk', [] ) ).toBe( false );

expect( isValueValid( 'hulk', [ 'iron man', 'hulk' ] ) ).toBe( true );

导入的依赖

我们的代码通常会在多处使用从外部和内部库导入的方法和属性,这使得通过传参来处理变得混乱且不切实际。对于这些情况,请使用 vi.mock 来替换模块的导出。

此示例在 config.js 中使用了一个具名导出,并配合一个读取该导出的模块:

// config.js
export const isEnabled = () => false;
// bilbo.js
import { isEnabled } from './config';

export const isBilboVisible = () => ! isEnabled( 'the-ring' );
// test/bilbo.test.js
import { beforeEach, describe, expect, test, vi } from 'vitest';
import { isEnabled } from '../config';
import { isBilboVisible } from '../bilbo';

vi.mock( import( '../config' ), () => ( {
    isEnabled: vi.fn(),
} ) );

beforeEach( () => {
    vi.mocked( isEnabled ).mockReturnValue( false );
} );

describe( 'The bilbo module', () => {
    test( 'shows Bilbo when the ring is disabled', () => {
        expect( isBilboVisible() ).toBe( true );
    } );

    test( 'hides Bilbo when the ring is enabled', () => {
        vi.mocked( isEnabled ).mockImplementationOnce(
            ( name ) => name === 'the-ring'
        );
        expect( isBilboVisible() ).toBe( false );
    } );
} );

vi.mock 会被提升(hoist)。请返回一个包含被替换导出的对象,对于默认导出则包含一个 default 键。如果工厂函数需要共享 mock 值,请使用 vi.hoisted。Gutenberg 会在测试之间重置 mock 实现并恢复 spy,因此请在 beforeEach 或测试主体中配置 mock。导入的模块状态是相互独立的;需要时请显式重置。

测试全局变量

对已有的方法使用 vi.spyOn,并在测试后将其恢复。下面这个 jsdom 示例在本地替换了 window.open:

// test/window.jsdom.test.js
import { afterEach, expect, test, vi } from 'vitest';

afterEach( () => {
    vi.restoreAllMocks();
} );

test( 'opens the requested page', () => {
    const open = vi.spyOn( window, 'open' ).mockReturnValue( null );
    window.open( '/example' );
    expect( open ).toHaveBeenCalledWith( '/example' );
} );

在替换全局值或环境值时,请使用 vi.stubGlobal 或 vi.stubEnv。将确定性的浏览器 mock 保留在 jsdom 测试内部,并对其进行恢复。不要为了规避 Browser Mode 而 mock 布局或浏览器交互。

用户交互

要获得确定性的 jsdom 行为,请使用 React Testing Library 和 @testing-library/user-event。Gutenberg 提供了 DOM 匹配器设置和 React 清理。此示例检查 DOM change 事件报告的值,而不对布局或原生焦点行为进行断言:

// test/input.jsdom.test.jsx
import { expect, test, vi } from 'vitest';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';

test( 'reports the typed value', async () => {
    const user = userEvent.setup();
    const onChange = vi.fn();
    render(
        <input
            aria-label="Title"
            onChange={ ( event ) => onChange( event.target.value ) }
        />
    );

    await user.type(
        screen.getByRole( 'textbox', { name: 'Title' } ),
        'Hello'
    );
    expect( onChange ).toHaveBeenLastCalledWith( 'Hello' );
} );

对于原生交互或渲染后的行为,请使用 Browser Mode。从 vitest-browser-react 导入并 await render 或 renderHook。从 vitest/browser 导入 userEvent,并 await 其操作。对于异步浏览器状态,优先使用定位器(locator)和 await expect.element( locator )。在 Browser Mode 没有等效功能的地方,可以继续使用 Testing Library 查询辅助函数;其 React 渲染器专用于 jsdom 以及下文中的共享集成辅助函数。

// test/button.browser.test.jsx
import { expect, test, vi } from 'vitest';
import { userEvent } from 'vitest/browser';
import { render } from 'vitest-browser-react';

test( 'calls the handler when Save is clicked', async () => {
    const onClick = vi.fn();
    const screen = await render( <button onClick={ onClick }>Save</button> );

    await userEvent.click( screen.getByRole( 'button', { name: 'Save' } ) );
    expect( onClick ).toHaveBeenCalledTimes( 1 );
} );

仅当构造特定事件本身就是被测试的行为时,才使用合成的 fireEvent。不要用它来代替浏览器交互。

区块 UI 的集成测试

集成测试将某个区块或编辑器行为所需的组件作为一个整体进行渲染。它们使用单元测试命令运行。其文件名决定了是使用 jsdom 还是 Browser Mode。这些测试会在special instance of the block editor中渲染区块。

这种方法无需完整的端到端框架即可测试大部分区块编辑器行为。当集成测试依赖于浏览器渲染或原生输入时,请使用 Browser Mode。对于需要完整 WordPress 站点或页面间导航的行为,请保留端到端测试。

The Cover block 是一个使用这一级别测试的区块示例,它为很大一部分编辑器交互提供了测试覆盖。

要设置 Browser Mode 集成测试:

import { initializeEditor } from 'test/integration/helpers/integration-test-editor';

async function setup( attributes ) {
    const testBlock = { name: 'core/cover', attributes };
    return initializeEditor( testBlock );
}

initializeEditor 函数返回 @testing-library/react 的 render 方法的输出。这个共享辅助函数是标准 Browser Mode 渲染器仅存的例外。新的直接 React Browser Mode 测试必须从 vitest-browser-react 导入并 await render。该辅助函数还接受一个区块元数据对象数组,允许你用多个区块来设置编辑器。

集成测试编辑器模块还导出了一个 selectBlock,可用于通过区块包装器上的 aria-label 来选择要测试的区块,例如 “Block: Cover”。

快照测试

这是对快照测试的概述,并介绍了如何最好地利用快照测试。

TL;DR 快照损坏

当快照测试失败时,仅仅意味着组件的渲染发生了变化。如果这种变化并非有意为之,那么快照测试就刚好防止了一个 bug 😊

但是,如果更改是有意的,请按照以下步骤更新快照。运行以下命令来更新快照:

# 为匹配的单元或集成测试更新快照。
npm run test:unit:update -- path/to/tests

# 为 e2e 测试更新快照
npm run test:e2e -- --update-snapshots path/to/spec
  1. 审查 diff,并确保这些更改是预期且有意为之的。
  2. 提交。

什么是快照?

快照只是由测试生成的某种数据结构的一种表示。快照存储在文件中,并与测试一起提交。运行测试时,会将生成的数据结构与文件中的快照进行比较。

创建快照非常容易:

import { expect, test } from 'vitest';

test( 'foobar test', () => {
    const foobar = { foo: 'bar' };

    expect( foobar ).toMatchSnapshot();
} );

这是生成的快照:

exports[ `foobar test 1` ] = `
{
  "foo": "bar",
}
`;

你绝不应该直接创建或修改快照,它们由测试生成和更新。

优点

  • 添加测试非常简单、简洁。
  • 防止意外更改。
  • 使用简单。
  • 无需运行应用程序即可揭示内部结构。

缺点

  • 缺乏表达力。
  • 只有在引入更改时才能捕获问题。
  • 对任何非确定性的内容都会出现问题。

使用场景

快照可以记录重构过程中组件结构的变化。这个 jsdom 示例使用了本地组件和显式的 Vitest 导入:

import { describe, expect, test } from 'vitest';
import { render, screen } from '@testing-library/react';

function SolarSystem( { planets = false } ) {
    return <div>{ planets && <span>Mars</span> }</div>;
}

describe( 'SolarSystem', () => {
    test( 'should render', () => {
        const { container } = render( <SolarSystem /> );

        expect( container ).toMatchSnapshot();
    } );

    test( 'should contain mars if planets is true', () => {
        const { container } = render( <SolarSystem planets /> );

        expect( container ).toMatchSnapshot();
        expect( screen.getByText( /mars/i ) ).toBeInTheDocument();
    } );
} );

Reducer 测试也非常适合快照。它们通常是大型、复杂且不应意外更改的数据结构,而这正是快照所擅长的!

使用快照

当快照不匹配时,CI 测试失败可能会让你措手不及。如果这些更改是预期的,你需要更新快照:

npm run test:unit:update -- path/to/tests

路径是可选的,但指定路径只会运行匹配的测试,并避免更新无关的快照。提交之前请先审查 diff。

在工作时,让聚焦的 watch 命令保持运行:

npm run test:unit:watch -- path/to/tests

当快照测试在 watch 模式下失败时,按下 u 即可更新它。

痛点

非确定性测试可能无法生成一致的快照,因此要当心。在处理任何随机、基于时间或其他非确定性的内容时,快照都会出现问题。

已连接(connected)组件的处理比较棘手。要对已连接组件进行快照,你可能需要导出未连接的组件:

// my-component.js
export { MyComponent };
export default connect( mapStateToProps )( MyComponent );

// test/my-component.js
import { MyComponent } from '..';
// 运行那些 MyComponent 测试…

已连接的 props 需要手动提供。这也是审查已连接状态的一个好机会。

最佳实践

如果你要开始一次重构,快照非常有用:你可以将它们作为分支上的第一个提交添加,然后观察其演变。

快照本身并不能表达我们的预期是什么。快照最好与描述我们预期的其他测试结合使用,就像上例所示:

import { expect, test } from 'vitest';
import { render, screen } from '@testing-library/react';

function SolarSystem( { planets = false } ) {
    return <div>{ planets && <span>Mars</span> }</div>;
}

test( 'should contain mars if planets is true', () => {
    const { container } = render( <SolarSystem planets /> );

    // 快照将捕获意外的更改
    expect( container ).toMatchSnapshot();

    // 这是我们实际期望在测试中找到的内容
    expect( screen.getByText( /mars/i ) ).toBeInTheDocument();
} );

与运行器无关的 toMatchDiffSnapshot 匹配器在 Gutenberg 中仍然可用。使用它可以在不记录两个完整快照的情况下比较两种状态。@testing-library/jest-dom 也仍然受支持;它的名称并不意味着它是 Jest 运行器的依赖项。

在 Browser Mode 中测试渲染样式。Node 和 jsdom 使用 CSS 模块代理,因此它们的快照无法确定计算样式的行为。Browser Mode 会加载由测试依赖图导入的样式。当 WordPress 通常会单独将某个包的全局样式表入队时,请在测试中显式导入该样式表。例如,对于 packages/components/src/button/test/ 中的测试:

import { expect, test } from 'vitest';
import { render } from 'vitest-browser-react';
import Button from '../';
import '../style.scss';

test( 'loads the button stylesheet', async () => {
    const screen = await render( <Button __next40pxDefaultSize>Save</Button> );
    await expect
        .element( screen.getByRole( 'button', { name: 'Save' } ) )
        .toHaveStyle( { height: '40px' } );
} );

调试 Vitest 单元测试

对于 Node 或 jsdom 测试,请运行聚焦的 npm run test:unit:debug -- path/to/test 命令。该命令会使用 --inspect-brk --no-file-parallelism --watch=false,在测试执行前暂停 worker,并打印其 inspector 地址。打开 chrome://inspect 或连接一个 Node inspector 客户端。恢复执行,然后在测试中使用断点或 debugger;。请参阅 scripts 调试器说明。

对于 Browser Mode,请以可见的浏览器运行聚焦测试,并打开其开发者工具:

npm run test:unit:watch -- path/to/example.browser.test.jsx --browser.headless=false

浏览器代码请使用浏览器的调试器;Node inspector 命令针对的是 Node worker。

Browser Mode 失败工件

Unit Tests 工作流中的 JavaScript Browser (Chromium, Node.js 24) 作业会上传失败截图。请在运行后三天内从该工作流运行的摘要页面下载 vitest-browser-failures 工件。只有当作业失败且存在文件时才会上传该工件;在浏览器测试开始之前发生的失败可能没有工件。

跟踪默认处于关闭状态。若要进行聚焦的诊断运行,请选择 Run workflow(运行工作流),选择失败的分支,并在 browser-trace-file 中输入一个相对于仓库路径的 *.browser.test.* 文件。Browser 作业会使用 Playwright 跟踪运行该文件,并在该文件失败或引发未处理的浏览器错误时保留其跟踪记录。其他作业保持其常规范围。这是一次独立的诊断运行,因此其结果并不能确定整个 Browser 测试套件是否通过。

该工件会保留以下相对于本地 test-results/ 目录的目录:

  • vitest-attachments/failure-screenshots/:自动生成的失败截图。
  • vitest-browser-screenshots/:使用 Browser Mode 截图 API 截取的截图(如果存在)。
  • vitest-browser-traces/:失败文件的 .trace.zip 归档,并保留其相对于仓库的路径。

测试输出会标识与每次失败对应的文件。使用图像查看器打开 PNG 截图。要检查跟踪记录,请使用解压后 ZIP 的绝对路径:

npm exec --no --workspace @wordpress/unit-tests -- playwright show-trace /absolute/path/to/example.trace.zip

Playwright Trace Viewer 会显示录制的 Playwright 操作、DOM 快照、截图和网络活动。每个归档涵盖一个测试文件,包括其 setup 和 teardown。使用测试失败输出标识相关操作;JavaScript 断言不会被记录为 Playwright 操作。

要在本地录制相同的诊断信息:

WP_VITEST_BROWSER_TRACE=1 npm run test:unit -- --project=browser path/to/example.browser.test.jsx

省略 WP_VITEST_BROWSER_TRACE 即可在不进行跟踪的情况下测量同一次运行。即使文件通过,跟踪也会增加运行时间和临时磁盘占用。来自通过且没有未处理错误的文件的录制将被丢弃,不会导出 ZIP。每次跟踪运行都会清除之前的跟踪输出;先前本地失败产生的截图可能会保留。跟踪使用 worker 现有的浏览器上下文,并保留 Vitest 的按文件 iframe 隔离。如果在重用上下文时,先前文件的录制仍处于活动状态,则会在下一次跟踪开始之前保留该录制。跟踪不会增加重试次数。请勿将其与 Vitest 的 --browser.trace 选项结合使用,该选项控制的是另一套独立的跟踪实现。

端到端测试

端到端测试使用 Playwright 作为测试框架。请参阅专门的端到端测试指南,了解最佳实践和详细说明。

使用 wp-env

如果你正在使用内置的本地环境,可以使用以下命令在本地运行 e2e 测试:

npm run test:e2e

或以交互方式运行

npm run test:e2e -- --ui

场景测试

如果你发现端到端测试在本地运行时通过,但在 GitHub Actions 中失败,你可以尝试通过模拟缓慢的 CPU 或网络来隔离受 CPU 或网络限制的竞态条件:

THROTTLE_CPU=4 npm run test:e2e

THROTTLE_CPU 是一个减速系数(在本示例中为 4 倍减速乘数)

参见 Chrome 文档:setCPUThrottlingRate

SLOW_NETWORK=true npm run test:e2e

SLOW_NETWORK 模拟相当于 Chrome 开发者工具中 “Fast 3G” 的网络速度。

参见 Chrome 文档:emulateNetworkConditions 和 NetworkManager.js

OFFLINE=true npm run test:e2e

OFFLINE 模拟网络断开。

参见 Chrome 文档:emulateNetworkConditions

核心区块测试

每个核心区块都必须为其主 save 函数提供至少一组 fixture 文件,并为每个弃用版本各提供一组。这些 fixture 用于测试区块的解析和序列化。更多信息与操作说明请参阅 集成测试 fixtures 自述文件。

不稳定测试

如果一个测试在没有任何代码更改的情况下,在多次重试尝试中既可能通过也可能失败,则该测试被视为不稳定。为了检测这类测试,我们在 CI 上最多自动重试失败的测试两次,并通过 report-flaky-tests GitHub action 将这些测试及其错误作为一条评论报告在拉取请求上。请注意,连续失败三次的测试不计为不稳定测试,不会被报告;此外,不稳定测试仅在拉取请求上报告。

PHP 测试

PHP 测试使用 PHPUnit 作为测试框架。如果你正在使用内置的本地环境,可以使用以下命令在本地运行 PHP 测试:

npm run test:php

若要在文件更改时自动重新运行测试(类似于 Vitest),请运行:

npm run test:php:watch

注意:phpunit 命令要求 wp-env 处于运行状态,且 composer 依赖已安装。如果 wp-env 尚未运行,包脚本会为你启动它。

在其他环境中,请运行 composer run test 和 composer run test:watch。

PHP 的代码风格通过 PHP_CodeSniffer 强制执行。建议你使用 Composer 安装 PHP_CodeSniffer 和 WordPress Coding Standards for PHP_CodeSniffer 规则集。安装 Composer 后,在项目目录中运行 composer install 以安装依赖项。上面的 npm run test:php 会同时执行单元测试和代码检查。可以单独运行 npm run lint:php 来验证代码检查。

若要仅运行单元测试而不运行 linter,请改用 npm run test:unit:php。

测试带前缀的函数

Gutenberg 的构建系统会自动为 PHP 函数添加 gutenberg_ 前缀,以避免与 WordPress 核心冲突。在为区块函数编写测试时,你必须测试构建后(带前缀)的版本,而不是源码版本。

如果这些测试被向后移植到 WordPress 核心,则必须将其更新为测试不带前缀的函数。

为带前缀的函数和类编写测试

在 PHPUnit 测试中,务必测试构建后(带前缀)的函数名和类名:

// phpunit/blocks/my-block-test.php
class My_Block_Test extends WP_UnitTestCase {
    public function test_my_function() {
        // 测试构建后的函数(带 gutenberg_ 前缀)
        $result = gutenberg_block_core_my_block_render_function( $args );
        $this->assertEquals( $expected, $result );
    }

    public function test_my_class() {
        // 测试构建后的类(带 _Gutenberg 后缀)
        $handler = new WP_Example_Block_Handler_Gutenberg();
        $result = $handler->process( $input );
        $this->assertEquals( $expected, $result );
    }
}

有关构建系统和函数前缀的更多详细信息,请参阅构建系统:函数前缀与区块加载文档。

性能测试

为了确保编辑器在添加功能的同时保持高性能,我们会监控拉取请求和版本发布可能对一些关键指标产生的影响,包括:

  • 编辑器加载所需的时间。
  • 键入时浏览器响应所需的时间。
  • 选择区块所需的时间。

性能测试是运行编辑器并捕获这些度量数据的端到端测试。请确保你已准备好 e2e 测试环境。

要设置 e2e 测试环境,请检出 Gutenberg 仓库并切换到你想测试的分支。运行以下命令来准备环境。

nvm use && npm install
npm run build

要运行测试,请运行以下命令:

npm run test:performance

这将为你提供当前分支/代码在运行环境中的结果。

此外,你还可以通过运行以下命令 npm exec --no release-cli -- perf [branches] 来比较不同分支(或标签或提交)之间的指标,例如:

npm exec --no release-cli -- perf trunk v8.1.0 v8.0.0

最后,你可以传递一个额外的 --tests-branch 参数,用于指定要运行哪个分支的性能测试文件。这在修改/扩展性能测试时特别有用:

npm exec --no release-cli -- perf trunk v8.1.0 v8.0.0 --tests-branch add/perf-tests-coverage

注意:此命令执行基准测试可能需要一些时间。在运行期间,请确保避免使用电脑或运行大量后台进程,以尽量减少可能影响各分支间结果的外部因素。