问题描述

我正在使用 TDD 开发一个插件,而我完全无法测试的一件事是钩子。

我的意思是 OK,我可以测试钩子回调,但是如何测试钩子是否实际触发 (两个自定义钩子和 WordPress 默认钩子)?我假设有些嘲弄会有所帮助,但是我根本找不到我失踪的东西。

我安装了测试套件与 WP-CLI 。根据 this answerinit 钩应该触发,但… 它不会; 代码也在 WordPress 里面工作。

从我的理解,引导程序最后被加载,所以没有触发 init 是有意义的,所以剩下的问题是:我应该如何测试钩子是否被触发?

谢谢!

引导文件如下所示:

$_tests_dir = getenv('WP_TESTS_DIR');
if ( !$_tests_dir ) $_tests_dir = '/tmp/wordpress-tests-lib';

require_once $_tests_dir . '/includes/functions.php';

function _manually_load_plugin() {
  require dirname( __FILE__ ) . '/../includes/RegisterCustomPostType.php';
}
tests_add_filter( 'muplugins_loaded', '_manually_load_plugin' );

require $_tests_dir . '/includes/bootstrap.php';

测试文件看起来像这样:

class RegisterCustomPostType {
  function __construct()
  {
    add_action( 'init', array( $this, 'register_post_type' ) );
  }

  public function register_post_type()
  {
    register_post_type( 'foo' );
  }
}

测试本身:

class CustomPostTypes extends WP_UnitTestCase {
  function test_custom_post_type_creation()
  {
    $this->assertTrue( post_type_exists( 'foo' ) );
  }
}

谢谢!

最佳解决方案

隔离测试

开发插件时,测试它的最佳方式是不加载 WordPress 环境。

如果您编写的代码可以轻松测试,无需 WordPress,您的代码变得更好。

单元测试的每个组件都应该被隔离测试:当你测试一个类时,只需要测试那个特定的类,假设所有其他代码都是完美的。

这就是为什么单元测试称为”unit” 的原因。

作为一个额外的好处,没有加载核心,你的测试将运行得更快。

避免构造函数中的钩子

我可以给你的一个提示是避免在构造函数中挂钩。这将使您的代码可以孤立地进行测试。

我们来看看 OP 中的测试代码:

class CustomPostTypes extends WP_UnitTestCase {
  function test_custom_post_type_creation() {
    $this->assertTrue( post_type_exists( 'foo' ) );
  }
}

我们假设这个测试失败了。谁是罪魁祸首?

  • 钩子根本没有添加或不正确?

  • 注册帖子类型的方法根本没有被调用或者是错误的参数?

  • WordPress 中有错误?

如何改善?

我们假设你的类代码是:

class RegisterCustomPostType {

  function init() {
    add_action( 'init', array( $this, 'register_post_type' ) );
  }

  public function register_post_type() {
    register_post_type( 'foo' );
  }
}

(注:我将参考该版本的其余答案)

我写这个类的方法允许你创建类的实例而不调用 add_action

在上面的课上有两件事要测试:

  • 方法 init 实际上调用 add_action 传递给它正确的参数

  • 方法 register_post_type 实际调用 register_post_type 函数

我没有说你必须检查帖子类型是否存在:如果你添加了正确的操作,并且如果你调用 register_post_type,那么定制的 post 类型必须存在:如果它不存在它是一个 WordPress 的问题。

记住:当你测试你的插件,你必须测试你的代码,而不是 WordPress 代码。在你的测试中,你必须假设 WordPress(就像你使用的任何其他外部库一样) 运行良好。这就是单元测试的意义。

但在实践中?

如果没有加载 WordPress,如果您尝试调用上面的类方法,您会发生致命错误,因此您需要模拟这些功能。

“manual” 方法

当然可以写你的嘲笑 Library 或”manually” 模拟各种方法。这是可能的。我会告诉你如何做到这一点,但是我会给你一个简单的方法。

如果 WordPress 在测试运行时未加载,则意味着您可以重新定义其功能,例如 add_actionregister_post_type

我们假设你有一个文件,从你的引导文件加载,你有:

function add_action() {
  global $counter;
  if ( ! isset($counter['add_action']) ) {
    $counter['add_action'] = array();
  }
  $counter['add_action'][] = func_get_args();
}

function register_post_type() {
  global $counter;
  if ( ! isset($counter['register_post_type']) ) {
    $counter['register_post_type'] = array();
  }
  $counter['register_post_type'][] = func_get_args();
}

我将 re-wrote 的功能简单地在每次调用时将元素添加到全局数组。

现在您应该创建 (如果您还没有) 您自己的基础测试用例类扩展了 PHPUnit_Framework_TestCase:它允许您轻松配置您的测试。

它可以是:

class Custom_TestCase extends PHPUnit_Framework_TestCase {

    public function setUp() {
        $GLOBALS['counter'] = array();
    }

}

以这种方式,在每次测试之前,全局计数器被复位。

现在你的测试代码 (我参考我上面发布的重写类):

class CustomPostTypes extends Custom_TestCase {

  function test_init() {
     global $counter;
     $r = new RegisterCustomPostType;
     $r->init();
     $this->assertSame(
       $counter['add_action'][0],
       array( 'init', array( $r, 'register_post_type' ) )
     );
  }

  function test_register_post_type() {
     global $counter;
     $r = new RegisterCustomPostType;
     $r->register_post_type();
     $this->assertSame( $counter['register_post_type'][0], array( 'foo' ) );
  }

}

你应该注意:

  • 我能够单独调用这两种方法,而 WordPress 根本就没有加载。这样一来,如果一个测试失败,我就知道究竟是谁的罪魁祸首。

  • 正如我所说,这里我测试类调用 WP 函数与期望的参数。没有必要测试 CPT 是否真的存在。如果您正在测试 CPT 的存在,那么您正在测试 WordPress 行为,而不是您的插件行为…

尼斯.. 但它是一个 PITA!

是的,如果你必须手动模拟所有的 WordPress 功能,这真的是一个痛苦。我可以给出的一些一般建议是使用尽可能少的 WP 函数:您不必重写 WordPress,而是您在自定义类中使用的抽象 WP 函数,以便它们可以被嘲笑和轻松测试。

例如。关于上面的例子,您可以编写一个注册帖子类型的类,在给定的参数的’init’ 上调用 register_post_type 。有了这个抽象,您仍然需要测试该类,但是在您注册帖子类型的代码的其他地方,您可以使用该类,在测试中嘲笑它 (因此假设它有效) 。

令人敬畏的是,如果您编写一个抽象 CPT 注册的类,您可以为其创建一个单独的存储库,并感谢 Composer 等现代工具将其嵌入到所有需要它的项目中:测试一次,使用无处不在。如果你发现了一个 bug,你可以在一个地方修复它,并用一个简单的 composer update,所有使用它们的项目都是固定的。

第二次:编写可隔离的代码是写入更好的代码。

但迟早我需要在某个地方使用 WP 功能

当然。你永远不应该与核心并行,这是没有道理的。你可以编写包含 WP 函数的类,但是这些类也需要测试。上述的”manual” 方法可以用于非常简单的任务,但是当一个类包含大量的 WP 函数时,这可能是一个痛苦。

幸运的是,那里有好的人写好东西。最大的 WP 机构之一的 10up 为想要正确测试插件的人们提供了一个非常好的 Library 。它是 WP_Mock

它允许你模拟 WP 功能的钩子。假设你已经加载了你的测试 (见 repo 自述),我上面写的同样的测试成为:

class CustomPostTypes extends Custom_TestCase {

  function test_init() {
     $r = new RegisterCustomPostType;
     // tests that the action was added with given arguments
     WP_Mock::expectActionAdded( 'init', array( $r, 'register_post_type' ) );
     $r->init();
  }

  function test_register_post_type() {
     // tests that the function was called with given arguments and run once
     WP_Mock::wpFunction( 'register_post_type', array(
        'times' => 1,
        'args' => array( 'foo' ),
     ) );
     $r = new RegisterCustomPostType;
     $r->register_post_type();
  }

}

简单,不是吗?这个答案不是 WP_Mock 的教程,所以阅读 repo readme 了解更多信息,但上面的例子应该很清楚,我想。

此外,您不需要自己编写任何嘲弄的 add_actionregister_post_type,或保留任何全局变量。

和 WP 类?

WP 也有一些类,如果运行测试时没有加载 WordPress,则需要模拟它们。

这比嘲笑功能容易得多,PHPUnit 有一个嵌入式系统来模拟对象,但是在这里我想向你推荐 Mockery 。这是一个非常强大的 Library ,非常易于使用。此外,它是 WP_Mock 的依赖,所以如果你有它,你也有 Mockery 。

但是 WP_UnitTestCase 怎么样?

WordPress 测试套件是为测试 WordPress 核心而创建的,如果您希望对核心做出贡献,那么它是至关重要的,但是使用它来进行插件只会使您无法孤立地进行测试。

把你的眼睛放在 WP 世界:现在有很多 PHP 框架和 CMS,而且没有一个建议使用框架代码来测试插件/模块/扩展 (或任何它们被称为) 。

如果你错过工厂,这个套件的一个有用的功能,你必须知道那里有 awesome things

奇迹和缺点

有一种情况,我在这里建议的工作流程缺少:自定义数据库测试。

实际上,如果您使用标准的 WordPress 表和函数来写入 (在最低级别的 $wpdb 方法),则您无需实际写入数据或测试数据是否在数据库中,只需确保使用适当的参数调用正确的方法。

但是,您可以使用自定义表和函数编写插件,从而构建查询写入,并测试这些查询是否是您的责任。

在这些情况下,WordPress 测试套件可以帮助您很多,并且在某些情况下可能需要加载 WordPress 来运行像 dbDelta 这样的功能。

(没有必要说使用不同的 db 进行测试,不是吗?)

幸运的是,PHPUnit 允许您组织您可以单独运行的”suites” 中的测试,因此您可以编写一个用于自定义数据库测试的套件,您可以在其中加载 WordPress 环境 (或其中一部分),留下所有其余测试 WordPress-free 。

只需要写一些尽可能抽象尽可能多的数据库操作的类,这样所有其他插件类都可以使用它们,这样使用 mock 就可以正确地测试大多数类而不处理数据库。

第三次,编写代码可以轻松测试隔离意味着编写更好的代码。

参考文献

注:本文内容整合自 Google/Baidu/Bing 辅助翻译的英文资料结果。如果您对结果不满意,可以加入我们改善翻译效果:薇晓朵技术论坛。