我正在开发使用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吗?

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

@Sven:是的,测试失败了;我安装了bin / install-wp-tests.sh(因为我使用过wp-cli)@ J.D .:调用RegisterCustomPostType :: __ construct(只是添加了die()语句并且phpunit在那里停止)

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

@Rarst:感谢您的建议,但仍然无法正常工作。出于某种原因,我认为计时不正确(测试是在进行初始化钩子之前运行的)。

#1 楼

隔离测试
开发插件时,最好的测试方法是不加载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行为,而不是插件行为...

很好..但是它是PITA!
是的,如果您必须手动操作模拟所有的WordPress函数,这确实很痛苦。
我能提供的一些一般性建议是使用尽可能少的WP函数:您不必重写WordPress,但可以在自定义类中使用抽象的WP函数,以便可以对其进行嘲笑并轻松对其进行测试。
例如关于上面的示例,您可以编写一个注册帖子类型的类,并使用给定的参数在'init'上调用register_post_type
有了这种抽象,您仍然需要测试该类,但是在代码中注册帖子类型的其他地方,您可以使用该类,在测试中对其进行模拟(因此假设它可以工作)。
棒的是,如果您编写了一个抽象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核心,如果您想为核心做贡献它至关重要,但是将其用于插件只会使您无法孤立地进行测试。
将目光投向WP world:那里有很多现代的PHP框架和CMS,但它们都没有建议测试插件/模块/
如果错过了工厂,这是套件的一项有用功能,那么您必须知道那里有很棒的东西。
陷阱和缺点
在某些情况下,我在这里建议的工作流程会缺少:自定义数据库测试。
实际上,如果您使用标准的WordPress表和函数在此处编写(最低级别的$wpdb方法),则实际上不需要写数据或测试数据是否确实在数据库中,只需确保使用正确的argume调用正确的方法nts。
但是,您可以编写带有自定义表和函数的插件来构建要写入的查询,并测试这些查询是否有效是您的责任。
在这种情况下,WordPress测试套件可以为您提供很多帮助,在某些情况下,可能需要加载WordPress才能运行dbDelta之类的功能。
(不必说使用其他数据库进行测试,不是吗?)
幸运的是,PHPUnit允许您组织您的测试可以在“套件”中单独运行,因此您可以编写一个用于自定义数据库测试的套件,在该套件中加载WordPress环境(或其中的一部分),而其余所有测试都无需WordPress。
只有确保编写的类可以抽象出尽可能多的数据库操作,所有其他插件类都应使用它们,以便使用模拟功能,您可以正确地测试大多数类,而无需处理数据库。
第三次,编写易于隔离测试的代码意味着编写更好的代码。

评论


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

–爱奥努特(Ionut Staicu)
2014年10月11日15:08

谢谢@IonutStaicu我忘了提一下,不加载WordPress可以使您的测试更快

– gmazzap♦
2014年10月11日15:13

还值得指出的是WP Core单元测试框架是运行INTEGRATION测试的绝佳工具,它将是自动化测试,以确保它与WP本身很好地集成(例如,不会发生偶然的功能名称冲突等)。

– John P Bloch
2014年10月11日19:15

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

– gmazzap♦
2014年10月11日20:17

感谢WordPress的UnitTest森林上的“直升机之旅”-我仍在笑那史诗般的照片;-)

– birgire
14-10-12在10:33