隔离测试
开发插件时,最好的测试方法是不加载WordPress环境。
如果您编写无需使用WordPress即可轻松测试的代码,则您的代码会变得更好。
应该对每个经过单元测试的组件进行隔离测试:在测试一个类时,假设所有其他代码都能正常工作,则只需测试该特定类。
这就是为什么将单元测试称为“单元”的原因。
另一个好处是,无需加载内核,您的测试将运行得更快。
避免在构造函数中使用钩子
我可以给您的提示是避免在构造函数中使用钩子。这就是使您的代码可独立测试的原因之一。
让我们看看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
,则自定义帖子类型必须存在:如果不存在,则是WordPress问题。
记住:当你测试你的插件,你必须测试你的代码,而不是WordPress的代码。在测试中,您必须假设WordPress(就像您使用的任何其他外部库一样)都能正常工作。那就是单元测试的意思。
但是...在实践中?
如果未加载WordPress,则尝试调用上述类方法时,会出现致命错误,因此需要模拟函数。
“手动”方法
当然,您可以编写自己的模拟库或“手动”模拟每种方法。这是可能的。我将告诉您如何执行此操作,但随后将向您显示一个更简单的方法。
如果在运行测试时未加载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();
}
我重新编写了函数,以在每次调用它们时将一个元素简单地添加到全局数组中。
现在,您应该创建(如果还没有的话)您自己的基本测试用例类扩展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行为,而不是插件行为...
很好..但是它是皮塔饼!
是的,如果您必须手动模拟所有WordPress功能,那确实很痛苦。我可以提供的一些一般性建议是,使用尽可能少的WP函数:您不必重写 WordPress,但是可以在自定义类中使用抽象的 WP函数,以便可以对它们进行模拟和轻松测试。
例如,关于上面的示例,您可以编写一个注册帖子类型的类,并register_post_type
使用给定参数调用“ init”。使用这种抽象,您仍然需要测试该类,但是在代码中注册帖子类型的其他地方,您可以使用该类,在测试中对其进行模拟(因此假设它可以工作)。
令人敬畏的是,如果您编写了一个抽象CPT注册的类,则可以为其创建一个单独的存储库,并且借助诸如Composer之类的现代工具,可以将其嵌入到需要的所有项目中:测试一次,在任何地方使用。而且,如果您发现了其中的错误,则可以将其修复在一个位置,并且只需简单地修复composer update
所有使用它的项目。
第二次:编写可隔离测试的代码意味着编写更好的代码。
但是迟早我需要在某个地方使用WP函数...
当然。您永远不应与核心并行,这没有任何意义。您可以编写包装WP函数的类,但是这些类也需要进行测试。上面描述的“手动”方法可以用于非常简单的任务,但是当一个类包含许多WP函数时,可能会很痛苦。
幸运的是,那边有好人写好东西。最大的WP机构之一10up,为希望以正确方式测试插件的人们提供了一个很好的库。是的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
,因此,请阅读回购自述文件以了解更多信息,但是我认为上面的示例应该很清楚。
此外,你不需要编写任何嘲笑 add_action
或register_post_type
自己,或维护任何全局变量。
和WP类?
WP也有一些类,如果在运行测试时未加载WordPress,则需要模拟它们。
这比模拟函数要容易得多,PHPUnit拥有一个嵌入式系统来模拟对象,但是在这里我想向您推荐Mockery。这是一个非常强大的库,非常易于使用。而且,它是的依赖项WP_Mock
,因此,如果有,也将具有Mockery。
但是呢WP_UnitTestCase
?
WordPress测试套件的创建是为了测试WordPress 核心,如果您想对WordPress 核心做出贡献,则它至关重要,但是将其用于插件只会使您无法孤立地进行测试。
放眼WP世界:有很多现代的PHP框架和CMS,但是没有一个建议使用框架代码测试插件/模块/扩展(或所谓的)。
如果您错过工厂,这是该套件的一项有用功能,则必须知道那里有很棒的东西。
陷阱和缺点
在某些情况下,我在这里建议的工作流程缺乏:自定义数据库测试。
实际上,如果您使用标准的WordPress表和函数在其中写入(最低级别的$wpdb
方法),则无需实际写入数据或测试数据是否真正存在于数据库中,只需确保使用正确的参数调用正确的方法即可。
但是,您可以编写带有自定义表和函数的插件,以构建要在其中编写查询的表,并测试这些查询是否有效由您负责。
在这种情况下,WordPress测试套件可以为您提供很多帮助,并且在某些情况下可能需要加载WordPress才能运行诸如之类的功能dbDelta
。
(不必说要使用其他数据库进行测试,不是吗?)
幸运的是,PHPUnit允许您在可以单独运行的“套件”中组织测试,因此您可以编写一个用于自定义数据库测试的套件,在该套件中加载WordPress环境(或其中的一部分),而其余所有测试都无需WordPress。
仅确保编写的类尽可能抽象一些数据库操作,而所有其他插件类都使用它们,以便使用模拟程序,您可以正确地测试大多数类,而无需处理数据库。
第三次,编写易于隔离测试的代码意味着编写更好的代码。
phpunit
,可以看到失败或通过的测试吗?您安装了bin/install-wp-tests.sh
吗?