测试钩子回调


34

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

我的意思是,可以测试钩子回调,但是如何测试钩子是否实际触发(自定义钩子和WordPress默认钩子)?我认为有些嘲笑会有所帮助,但我根本无法弄清我所缺少的内容。

我使用WP-CLI安装了测试套件。根据这个答案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' ) );
  }
}

谢谢!


如果您正在运行phpunit,可以看到失败或通过的测试吗?您安装了bin/install-wp-tests.sh吗?
2014年

我认为问题的一部分是,RegisterCustomPostType::__construct()在为测试加载插件时可能永远不会调用它。您还可能会受到#29827错误的影响;也许尝试更新您的WP单元测试套件的版本。
JD 2014年

@Sven:是的,测试失败了;我安装了bin/install-wp-tests.sh(因为我用的wp-CLI)@JD:RegisterCustomPostType :: __结构称为(只是增加了一个die()声明,PHPUnit的停在那里)
约努茨Staicu

我不太确定在单元测试方面(不是我的强项),但是从字面上看,您可以did_action()用来检查是否触发了动作。
腊斯特2014年

@Rarst:感谢您的建议,但仍然无法正常工作。出于某种原因,我认为时机错误(测试是在执行init钩子之前进行的)。
Ionut Staicu 2014年

Answers:


72

隔离测试

开发插件时,最好的测试方法是加载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_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();
}

我重新编写了函数,以在每次调用它们时将一个元素简单地添加到全局数组中。

现在,您应该创建(如果还没有的话)您自己的基本测试用例类扩展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_actionregister_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

仅确保编写的类尽可能抽象一些数据库操作,而所有其他插件类都使用它们,以便使用模拟程序,您可以正确地测试大多数类,而无需处理数据库。

第三次,编写易于隔离测试的代码意味着编写更好的代码。


5
废话,很多有用的信息!谢谢!我以某种方式设法错过了单元测试的整个要点(直到现在,我只在Code Dojo内练习PHP测试)。今天早些时候,我也发现了关于wp_mock的信息,但是由于某种原因,我设法忽略了它。令我感到恼火的是,无论测试多么小,它至少要花两秒钟才能运行(首先加载WP env,然后执行测试)。再次感谢您睁开眼睛!
Ionut Staicu 2014年

4
由于@IonutStaicu我忘了提到,不加载的WordPress,使您的测试了很多更快
gmazzap

6
还值得指出的是,WP Core单元测试框架是运行INTEGRATION测试的出色工具,它将是自动化测试,以确保它与WP本身很好地集成在一起(例如,不存在偶然的功能名称冲突等)。
约翰·布洛赫

1
@JohnPBloch +1为好点。即使使用名称空间足以避免WordPress中任何函数名称冲突,在WordPress中,函数都是全局的:)但是,可以肯定的是,集成/功能测试是一回事。我目前正在与Behat + Mink一起玩,但我仍在练习。
gmazzap

1
感谢WordPress的UnitTest森林上的“直升机之旅”-我仍在笑那史诗般的照片;-)
birgire
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.