社区新闻

如何为你的 WordPress 插件添加自动化单元测试

在开发 WordPress 插件时,确保代码质量不仅是最佳实践,更是构建健壮可靠插件的必要条件。用户的环境千差万别,涉及不同的 WordPress 版本、PHP 配置以及各种主题和相关插件的组合。如果没有可靠的验证系统,很容易遇到意外错误。在未来的版本中,你也可能引入回归问题,导致原本正常工作的部分出现故障。

将测试纳入你的工作流程可以防止回归,确保稳定性,并在每次新版本发布时建立信心。

在本教程中,你将逐步学习如何为 WordPress 插件添加自动化单元测试——从环境设置到与 GitHub 集成以自动运行测试。

什么是单元测试,为什么要在 WordPress 中使用它们?

单元测试自动验证代码的特定部分(通常是一个函数或方法)是否完全按预期工作。其思想是将该“单元”与系统的其余部分隔离,以确保其在所有你能想到的情况下行为正确。此外,该单元可以接收不同类型的输入以验证预期的输出。这些输入可能是有效数据,你也可以使用无效数据来查看函数的行为。这有助于你避免触发致命错误,否则会导致插件行为异常。

输入-函数-输出的基本图示。

在 WordPress 插件开发中,单元测试帮助你验证你的函数、类和 Hook 是否实现了其目的——即使在引入代码更改之后。这一点尤其重要,因为插件可能在非常不同的环境中运行:不同版本的 WordPress、多种 PHP 配置,以及其他插件和主题的无尽组合。

与其他类型测试的区别

了解可以执行的不同测试类型很重要,以便理解每种测试的范围和能做什么。

  • 单元测试: 隔离测试代码的各个部分,不依赖外部元素。
  • 集成测试: 验证系统的不同部分是否能良好协作,例如你的插件和 WordPress 数据库。这就是我们将在本文中涵盖的测试。
  • 功能测试: 从用户角度评估系统的行为。

在 WordPress 中运行测试的优势

使用测试是一种高级的软件开发方式,具有多重优势:

  • 防止回归: 如果你更新插件并破坏了原本正常工作的函数,单元测试会在你发布新版本之前立即通知你。
  • 提高可靠性和信心: 你可以更安心地发布新版本,因为知道你的关键函数已经过验证。
  • 使维护更容易: 当其他人参与开发协作时,他们可以在发布更改前知道是否破坏了某些功能。
  • 提高代码质量: 编写测试迫使你思考如何编写更清晰、可重用且易于调试的函数。
  • 改善用户体验: 错误由开发者在代码中预防,而不是被用户发现。
  • 帮助你记录代码。 它们是理解代码的良好参考。你将看到函数使用测试数据运行时会发生什么。

总之,WordPress 中的单元测试不是奢侈品,而是对稳定性和专业性的投资。在如此多样化的生态系统中,它们有助于确保你的插件在今天和明天都能正常工作。

在计算机上设置测试环境

集成测试通常是 WordPress 代码的最佳方法,因为它们在实际的 WordPress 环境中测试代码,而不仅仅是隔离测试。

要运行集成测试,你需要创建一个基于干净 WordPress 安装的环境,并具备必要的依赖项,如 PHP、MySQL 和 Subversion。

目前,有两种方法可以为开发测试设置环境:

  • NPM 和 Docker (wp-env)
  • Composer

按照本教程,你将使用 Composer 选项。它在运行测试时更直接、更快。

对于 Composer,你需要在计算机上安装 PHPMySQL,并创建一个名为 wordpress_tests 的数据库,用户名和密码设置为 root。

你还需要 SVN 来正确下载文件,因为安装测试是使用 SVN 运行的。Git 的新版本即将推出,但目前还不可用。最后,你需要使用 WordPress CLI 在我们的插件中创建测试文件。

为插件设置单元测试

本指南基于使用 PHPUnit 运行测试,通过 Composer 安装在仓库本身中。这是开发者在开发插件时共享相同环境的最简单方法。

你可以使用以下 插件示例 作为本教程的起点。 从其仓库克隆它 并将本文中的说明应用于它。

我建议你在编写单元测试时,边阅读教程边遵循这个插件示例。这将让你对如何在插件中实现单元测试有一个实际的了解。

让现有插件为测试做好准备

使用 WordPress CLI 命令配置你的插件,以安装运行测试所需的初始文件。将 plugin-name 替换为你的插件名称。

wp scaffold plugin-tests plugin-name --ci=github

运行 该命令 时,以下文件将添加到你的插件文件夹中:

  • .github/workflows/phpunit.yml → 与 GitHub Actions 集成。
  • bin/install-wp-tests.sh → 用于设置测试环境的 Bash 脚本。
  • phpunit.xml.dist → PHPUnit 配置。
  • tests/bootstrap.php → 在测试前加载 WordPress、配置和插件。在这里我们可以定义环境常量。
  • tests/test-sample.php → 示例测试。

安装用于测试的 Composer 依赖项

你还需要在仓库中安装 PHPUnit。你可以使用 Composer 安装它。

composer require --dev yoast/phpunit-polyfills:^1.0 wp-phpunit/wp-phpunit:^6.3

这将安装:

  • WP-PHPUNIT。一个使用 PHPUnit 的 WordPress 测试环境实现。这是一个非官方的、社区维护的分支,可以更轻松地运行测试,而无需手动克隆整个 WordPress 核心。
  • PHPUNIT-Polyfills。由 Yoast 创建的库,用于提供跨多个 PHPUnit 版本和不同 PHP 版本的兼容性。

在 Git 中忽略测试文件

由于你将把仓库放在 Git 中,你可能希望将不想出现的文件夹和文件添加到你的 .gitignore 文件中:

.phpunit.result.cache
vendor/

创建 Composer 测试脚本

Composer scripts 是可以在项目的 composer.json 文件中定义的自定义命令。它们让你可以自动化常见任务。

添加上运行测试和设置环境的命令,以使你的工作更轻松。使用 所需的凭据 自定义 test-install

"scripts": {
	"test": "phpunit",
	"test-install": "bash bin/install-wp-tests.sh wordpress_test root 'root' 127.0.0.1 latest"
}

这句话是什么意思?

bash bin/install-wp-tests.sh wordpress_test root 'root' 127.0.0.1 latest

这意味着安装一个用于测试目的的 WordPress 安装。

  • 数据库:wordpress_test
  • 用户名:root
  • 密码:root
  • 主机:localhost
  • WordPress 版本:latest

创建 WordPress 测试环境

现在运行你的脚本来安装 WordPress 测试环境,以便你可以创建测试。

composer test-install

文件将在你的仓库中创建,但在运行测试之前,我建议进行调整以防止以后出现错误。

自定义 phpunit.xml.dist 文件,并从 <directory> 行中删除 prefix="test-",因为根据版本不同,它可能会导致冲突和错误。此外,创建一个子文件夹以将测试保存在 tests/Unit/ 中。你可以根据需要自定义此结构。

 <testsuites>
   <testsuite name="testing">
     <directory suffix=".php">./tests/Unit/</directory>
   </testsuite>
 </testsuites>

将你应该在 /tests 文件夹下的 test-example.php 文件移动到 /Unit 子文件夹。

最后,在引导文件的开头添加这些行。它们定义了一个常量,用于存储输入数据,也有助于防止 WP_CORE_DIR 常量出错。

define( 'TESTS_PLUGIN_DIR', dirname( __DIR__ ) );
define( 'UNIT_TESTS_DATA_PLUGIN_DIR', TESTS_PLUGIN_DIR . '/tests/Data/' ); // Customize.

// Define WP_CORE_DIR if not already defined
if ( ! defined( 'WP_CORE_DIR' ) ) {
	$_wp_core_dir = getenv( 'WP_CORE_DIR' );
	if ( ! $_wp_core_dir ) {
		$_wp_core_dir = rtrim( sys_get_temp_dir(), '/\' ) . '/wordpress';
	}
	define( 'WP_CORE_DIR', $_wp_core_dir );
}

运行你的第一个单元测试

终于!现在你已准备好在 WordPress 插件中运行测试:

composer test

结果:

PHPUnit 9.6.24 by Sebastian Bergmann and contributors.
1 / 1 (100%)

Time: 00:00.005, Memory: 42.50 MB

OK (1 test, 1 assertion)

恭喜!你已安装单元测试并在插件中运行了第一个测试!

运行测试时遇到此错误吗?

如果在运行测试时遇到此错误,通常可以通过重新初始化所有文件夹并再次运行安装过程来修复。这通常能解决问题。

Could not find /wordpress-tests-lib/includes/functions.php, have you run bin/install-wp-tests.sh?

当临时的 WordPress 安装不完整时会发生这种情况。删除文件夹并重新运行安装。

当你运行测试环境安装时,它会告诉你 WordPress 的安装目录。例如,我有:/var/folders/kk/6287m8gj09xdkt2zgz432zhr0000gn/T/

你需要从此位置删除 wordpress-tests-libwordpress 文件夹,然后重新运行安装。

rm -rfv /var/folders/kk/6287m8gj09xdkt2zgz432zhr0000gn/T/wordpress-tests-lib/
rm -rfv /var/folders/kk/6287m8gj09xdkt2zgz432zhr0000gn/T/wordpress/
composer test-install

断言函数

你已经在插件中设置好了环境,现在是开发的时候了。为你的函数生成数据,运行它们,并验证预期结果是否正确。要检查这些结果,你可以使用 PHPUnit 的断言函数

以下是你在 PHPUnit 中最常用的 断言函数

  • assertTrue($condition) → 检查条件是否为真。
  • assertFalse($condition) → 检查条件是否为假。
  • assertEquals($expected, $actual) → 验证两个值是否相等(非严格)。
  • assertSame($expected, $actual) → 验证两个值是否相同(严格:类型和值)。
  • assertNotEquals($expected, $actual) → 验证两个值是否不相等。
  • assertNotSame($expected, $actual) → 验证两个值是否不相同(严格)。
  • assertNull($variable) → 检查变量是否为 null。
  • assertNotNull($variable) → 检查变量是否不为 null。
  • assertEmpty($variable) → 检查变量是否为空。
  • assertNotEmpty($variable) → 检查变量是否不为空。
  • assertCount($expectedCount, $array) → 检查数组(或可计数对象)是否具有预期数量的元素。
  • assertContains($needle, $haystack) → 检查值是否存在于数组或字符串中。
  • assertNotContains($needle, $haystack) → 检查值是否不存在于数组或字符串中。
  • assertInstanceOf($class, $object) → 验证对象是否是给定类的实例。
  • assertIsArray($variable), assertIsString($variable), assertIsInt($variable), 等。 → 类型断言。
  • assertGreaterThan($expected, $actual) → 检查一个值是否大于另一个值。
  • assertLessThan($expected, $actual) → 检查一个值是否小于另一个值。

这些是最常用的,但 PHPUnit 为特殊情况提供了更多断言。

我建议同时使用正确的输入数据以及不正确或缺失的数据来运行函数。这样,你的函数会变得更健壮,并为不同类型的输入做好准备。没有用户喜欢在使用插件时看到致命错误。

编写你的第一个测试

在我们的插件示例中,我们可以 为此函数编写断言,该函数位于我们的插件中:

function plunit_sum( $a, $b ) {
	return $a + $b;
}

编写类似这样的内容并将其添加到你的测试文件中。在我们的示例中,我们将其放在 tests/Unit/test-example.php 中。编写类似这样的内容,你需要将其添加到你的测试文件中。在我们的示例中,我们使用:

function test_sum_without_errors( $a, $b ) {
	$sum = plunit_sum( 4, 2 );
	$this->assertEquals( 6, $sum );
}

这个测试将通过。这是理想情况,但如果用户输入字符串而不是数字怎么办?我们可以创建一个包含函数可能遇到的所有错误的示例。

function test_sum_with_errors( $a, $b ) {
	$sum = plunit_sum( 'hello', 2 );
	$this->assertEquals( 0, $sum );
}

当函数接收到不正确或意外的数据时,我们的单元测试应该捕获这一点。测试的目的是确保即使在给定无效输入时,函数也能正确运行——例如,通过返回错误、抛出异常或安全地处理该值,而不是静默失败。

每次你想运行测试时,都需要运行测试命令。

composer test

但让我们做一些更有趣的测试……

测试我们的插件功能

在我们的插件中,我们 注册了一个自定义文章类型,我们希望确保它被正确注册,并且你可以为此文章类型创建文章。

<?php
/**
 * Integration tests for the Book custom post type.
 */

class Test_Book_CPT extends WP_UnitTestCase {

	public function setUp(): void {
		parent::setUp();
		// Make sure our CPT is registered before each test.
		mtp_register_cpt_book();
		flush_rewrite_rules();
	}

	public function test_book_post_type_is_registered() {
		$this->assertTrue( post_type_exists( 'book' ), 'The "book" post type should be registered.' );
	}

	public function test_book_post_type_is_public() {
		$post_type_obj = get_post_type_object( 'book' );

		$this->assertNotNull( $post_type_obj );
		$this->assertTrue( $post_type_obj->public );
		$this->assertTrue( $post_type_obj->show_ui );
	}

	public function test_can_create_book_post() {
		$post_id = self::factory()->post->create(
			array(
				'post_type'  => 'book',
				'post_title' => 'Test Book',
			)
		);

		$this->assertIsInt( $post_id );
		$this->assertSame( 'book', get_post_type( $post_id ) );
		$this->assertSame( 'Test Book', get_the_title( $post_id ) );
	}
}

这个测试类验证了我们插件的 Book 自定义文章类型 (CPT) 是否正确注册、是否公开、是否显示在管理界面中,以及我们是否可以创建该类型的文章。我们将逐步讲解结构和每个测试,以便读者理解正在断言的行为以及使用了哪些 WP 测试辅助函数。

这个测试类验证了我们插件的 Book 自定义文章类型 (CPT) 是否正确注册、是否公开、是否显示在管理界面中,以及我们是否可以创建该类型的文章。我们将逐步讲解结构和每个测试,以便读者理解正在断言的行为以及使用了哪些 WP 测试辅助函数。

测试类别及其延伸原因WP_UnitTestCase

<?php
class Test_Book_CPT extends WP_UnitTestCase {
    ...
}

我们之所以扩展,是因为它提供了 PHPUnit 断言以及 WordPress 专用的辅助工具和测试隔离功能。这意味着我们可以使用熟悉的PHPUnit方法和WordPress测试工具,比如创建帖子、用户、术语等的对象。扩展还确保每个测试都能在WordPress测试环境中运行(夹具、数据库事务处理等)。WP_UnitTestCase$this->assertTrue()factory()WP_UnitTestCase

setUp()——为每场测试准备环境

public function setUp(): void {
    parent::setUp();
    // Make sure our CPT is registered before each test.
    mtp_register_cpt_book();
    flush_rewrite_rules();
}

setUp()在该类的每一次测试之前运行。这保证了起点的一致性,避免了测试之间的相互依赖。在这种情况下,我们:

  • 调用让基类自行执行配置(重要)。parent::setUp()
  • 调用以确保注册书中 CPT 的插件函数运行。测试通常单独运行,没有常规插件引导顺序,因此明确调用注册函数使 CPT 可用于下面的断言。mtp_register_cpt_book()
  • 调用刷新WordPress重写规则。在测试注册帖子类型时,刷新重写规则确保URL端点和与重写相关的标志是最新的;这是一个防御措施,确保依赖注册状态的测试不会受到陈旧重写规则的影响。flush_rewrite_rules()

提示:如果你的测试除了 CPT 之外,你可能还想取消注册或清理内容,但通常会处理已创建帖子的数据库清理。tearDown()WP_UnitTestCase

test_book_post_type_is_registered()— 基本存在性检查

public function test_book_post_type_is_registered() {
    $this->assertTrue( post_type_exists( 'book' ), 'The "book" post type should be registered.' );
}

它的作用:

  • 使用WordPress助手,当带有slug book的帖子类型存在时,该辅助工具会返回true。post_type_exists( 'book' )
  • 如果缺少 CPT,使用 PHPUnit 断言来测试失败。$this->assertTrue()

为什么这很重要:

  • 这是最简单、最根本的断言——如果帖子类型没有被注册,依赖它的插件功能就无法正常工作。

test_book_post_type_is_public()— 检查柱型属性

public function test_book_post_type_is_public() {
    $post_type_obj = get_post_type_object( 'book' );

    $this->assertNotNull( $post_type_obj );
    $this->assertTrue( $post_type_obj->public );
    $this->assertTrue( $post_type_obj->show_ui );
}

它的作用:

  • 调用以获取完整的帖子类型对象(带有注册 args 的 stdClass)。get_post_type_object('book')
  • 断言该对象不是零(额外安全)。
  • 断言是——意味着CPT旨在公开查询。$post_type_obj->publictrue
  • 断言是——意味着CPT显示在管理员界面(菜单/屏幕)中。$post_type_obj->show_uitrue

为什么这很重要:

  • 仅仅注册CPT是不够的——注册参数决定了可见性和行为。这些断言确认了CPT会显示在管理员中,并表现得像普通的公开帖子类型。

test_can_create_book_post()— 创建并验证CPT职位

public function test_can_create_book_post() {
    $post_id = self::factory()->post->create(
        array(
            'post_type'  => 'book',
            'post_title' => 'Test Book',
        )
    );

    $this->assertIsInt( $post_id );
    $this->assertSame( 'book', get_post_type( $post_id ) );
    $this->assertSame( 'Test Book', get_the_title( $post_id ) );
}

它的作用:

  • 使用WordPress测试工厂在测试数据库中创建帖子。 方便之处在于它绕过了常规的管理流程,直接插入帖子记录。self::factory()->post->create()factory()
  • 检查返回的帖子是否为整数(创建的帖子ID)。$post_id
  • 验证返回,确认创建的帖子类型正确。get_post_type($post_id)'book'
  • 验证等于提供的标题。get_the_title($post_id)'Test Book'

为什么这很重要:

  • 端到端确认 CPT 实际上可以使用:可以创建该类型的帖子,其数据按预期存储和检索。

把这些数据整合起来——这些测试保证了什么

这三项测试共同验证:

  1. CPT一书已注册。
  2. CPT具有预期的可见性和管理员界面标志。
  3. 你可以创建和检索 类型的帖子。book

这些检查轻便、易于维护,能发现常见错误(注册塞拉格中的错别字、错误的标志,或插件引导过程中从未运行的注册)。public/show_ui

你可以用AI自动化我们所学到的一切。你可以用这些说明,直接为插件创建测试。你只需要下载带有说明的文件,添加到你喜欢的IDE的AI聊天中,它就会应用之前的所有指令,甚至会建议一些单元测试。

完成单元测试

安装插件依赖,比如WooCommerce

如果你需要运行测试,而你的插件有依赖,你需要在 中添加插件安装,并且在 。install-wp-tests.sh_manually_load_plugin

让我们举个例子,如何将 WooCommerce 插件添加到你的测试中。在文件中,在行之前,添加下载 WooCommerce 插件所需的行作为依赖。bin/install-wp-tests.shinstall_wp

# Installs WooCommerce plugin in the test environment
install_woocommerce() {
	local PLUGIN_DIR="$WP_CORE_DIR/wp-content/plugins"
	mkdir -p "$PLUGIN_DIR"
	WOOCOMMERCE_URL="https://downloads.wordpress.org/plugin/woocommerce.zip"
	download "$WOOCOMMERCE_URL" "$TMPDIR/woocommerce.zip"
	unzip -q "$TMPDIR/woocommerce.zip" -d "$TMPDIR/"
	rm -rf "$PLUGIN_DIR/woocommerce"
	mv "$TMPDIR/woocommerce" "$PLUGIN_DIR/woocommerce"
	echo "WooCommerce plugin installed successfully."
}

在那行之后,添加该功能,使其在WordPress安装后立即运行。install_wpinstall_woocommerce

我们还需要把它包含在函数里的bootstrap里:tests/bootstrap.php_manually_load_plugin

// Load WooCommerce first from the standard WordPress plugins directory
require_once WP_CORE_DIR . '/wp-content/plugins/woocommerce/woocommerce.php';

在GitHub中设置自动化测试

在位于 的配置文件中,检查安装过程,检查仓库的主分支是否正确。如果不正确,你需要更新 YAML 文件:.github/workflows/testing.yml

on:
  pull_request:
    branches:
      - trunk

用你仓库的主分支替换trunk。

我还建议更新PHP设置线,因为支架有时会因PHP版本而出错。还有一点要注意,GitHub 里的 CI 不包含 SVN,所以你需要手动安装 SVN,因为它默认不包含。

     - name: Setup PHP
        uses: shivammathur/setup-php@v2
        with:
          php-version: ${{ matrix.php-version }}
          tools: phpunit-polyfills:1.1
          extensions: mbstring, xml, zip, intl, pdo, mysql
          coverage: none

      - name: Install SVN
        run: sudo apt-get update && sudo apt-get install -y subversion

结论

自动化单元测试是现代WordPress插件开发中不可或缺的实践。除了防止回归,它还增强了代码质量,提高了可维护性,并在发布新版本时建立了信心。通过将 PHPUnit、Composer 和 GitHub Actions 集成到工作流程中,你确保插件在不同环境中和未来更新中保持可靠性。归根结底,单元测试不仅仅是技术上的提升——它们代表了对稳定性、专业性以及为每位用户提供更好体验的承诺。