问题描述
我正在使用 TDD 开发一个插件,而我完全无法测试的一件事是钩子。
我的意思是 OK,我可以测试钩子回调,但是如何测试钩子是否实际触发 (两个自定义钩子和 WordPress 默认钩子)?我假设有些嘲弄会有所帮助,但是我根本找不到我失踪的东西。
我安装了测试套件与 WP-CLI 。根据 this answer,init
钩应该触发,但… 它不会; 代码也在 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_action
或 register_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_action
或 register_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 辅助翻译的英文资料结果。如果您对结果不满意,可以加入我们改善翻译效果:薇晓朵技术论坛。