Testing hooks callback

时间:2014-10-11 作者:Ionut Staicu

我正在开发一个使用TDD的插件,有一件事我完全没有测试出来,那就是。。。挂钩。

我的意思是好的,我可以测试钩子回调,但我如何测试钩子是否真的触发了(自定义钩子和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\' ) );
  }
}
谢谢!

1 个回复
最合适的回答,由SO网友:gmazzap 整理而成

Test in isolation

When developing a plugin, the best way to test it is without loading the WordPress environment.

If you write code that can be easily tested without WordPress, your code becomes better.

Every component that is unit tested, should be tested in isolation: when you test a class, you only have to test that specific class, assuming all other code is working perfectly.

The Isolator

This is the reason why unit tests are called "unit".

As an additional benefit, without loading core, your test will run much faster.

Avoid hooks in constructor

A tip I can give you is to avoid putting hooks in constructors. That\'s one of the things that will make your code testable in isolation.

Let\'s see test code in OP:

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

And let\'s assume this test fails. Who is the culprit?

  • the hook was not added at all or not properly?
  • the method that register the post type was not called at all or with wrong arguments?
  • there is a bug in WordPress?

How it can be improved?

Let\'s assume your class code is:

class RegisterCustomPostType {

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

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

(Note: I will refer to this version of the class for the rest of the answer)

The way I wrote this class allows you to create instances of the class without calling add_action.

In the class above there are 2 things to be tested:

  • the method init actually calls add_action passing to it proper arguments
  • the method register_post_type actually calls register_post_type function

I didn\'t say that you have to check if post type exists: if you add the proper action and if you call register_post_type, the custom post type must exist: if it doesn\'t exists it\'s a WordPress problem.

Remember: when you test your plugin you have to test your code, not WordPress code. In your tests you have to assume that WordPress (just like any other external library you use) works well. That\'s the meaning of unit test.

But... in practice?

If WordPress is not loaded, if you try to call class methods above, you get a fatal error, so you need to mock the functions.

The "manual" method

Sure you can write your mocking library or "manually" mock every method. It\'s possible. I\'ll tell you how to do that, but then I\'ll show you an easier method.

If WordPress is not loaded while tests are running, it means you can redefine its functions, e.g. add_action or register_post_type.

Let\'s assume you have a file, loaded from your bootstrap file, where you have:

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();
}

I re-wrote the functions to simply add an element to a global array every time they are called.

Now you should create (if you don\'t have one already) your own base test case class extending PHPUnit_Framework_TestCase: that allows you to easily configure your tests.

It can be something like:

class Custom_TestCase extends \\PHPUnit_Framework_TestCase {

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

}

In this way, before every test, the global counter is reset.

And now your test code (I refer to the rewritten class I posted above):

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\' ) );
  }

}

You should note:

  • I was able to call the two methods separately and WordPress is not loaded at all. This way if one test fails, I know exactly who the culprit is.
  • As I said, here I test that the classes call WP functions with expected arguments. There is no need to test if CPT really exists. If you are testing the existence of CPT, then you are testing WordPress behavior, not your plugin behavior...

Nice.. but it\'s a PITA!

Yes, if you have to manually mock all the WordPress functions, it\'s really a pain. Some general advice I can give is to use as few WP functions as possible: you don\'t have to rewrite WordPress, but abstract WP functions you use in custom classes, so that they can be mocked and easily tested.

E.g. regarding example above, you can write a class that registers post types, calling register_post_type on \'init\' with given arguments. With this abstraction you still need to test that class, but in other places of your code that register post types you can make use of that class, mocking it in tests (so assuming it works).

The awesome thing is, if you write a class that abstracts CPT registration, you can create a separate repository for it, and thanks to modern tools like Composer embed it in all the projects where you need it: test once, use everywhere. And if you ever find a bug in it, you can fix it in one place and with a simple composer update all the projects where it is used are fixed too.

For the second time: to write code that is testable in isolation means to write better code.

But sooner or later I need to use WP functions somewhere...

Of course. You never should act in parallel to core, it makes no sense. You can write classes that wraps WP functions, but those classes need to be tested too. The "manual" method described above may be used for very simple tasks, but when a class contains a lot of WP functions it can be a pain.

Luckily, over there there are good people that write good things. 10up, one of the biggest WP agencies, maintains a very great library for people that want to test plugins the right way. It is WP_Mock.

It allows you to mock WP functions an hooks. Assuming you have loaded in your tests (see repo readme) the same test I wrote above becomes:

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();
  }

}

Simple, isn\'t it? This answer is not a tutorial for WP_Mock, so read the repo readme for more info, but the example above should be pretty clear, I think.

Moreover, you don\'t need to write any mocked add_action or register_post_type by yourself, or maintain any global variables.

And WP classes?

WP has some classes too, and if WordPress is not loaded when you run tests, you need to mock them.

That\'s much easier than mocking functions, PHPUnit has an embedded system to mock objects, but here I want to suggest Mockery to you. It\'s a very powerful library and very easy to use. Moreover, it\'s a dependency of WP_Mock, so if you have it you have Mockery too.

But what about WP_UnitTestCase?

The WordPress test suite was created to test WordPress core, and if you want to contribute to core it is pivotal, but using it for plugins only makes you test not in isolation.

Put your eyes over WP world: there are a lot of modern PHP frameworks and CMS out there and none of them suggests testing plugin/modules/extensions (or whatever they are called) using framework code.

If you miss factories, a useful feature of the suite, you have to know that there are awesome things over there.

Gotchas and downsides

There is a case when the workflow I suggested here lacks: custom database testing.

In fact, if you use standard WordPress tables and functions to write there (at the lowest level $wpdb methods) you never need to actually write data or test if data is actually in database, just be sure that proper methods are called with proper arguments.

However, you can write plugins with custom tables and functions that build queries to write there, and test if those queries work it\'s your responsibility.

In those cases WordPress test suite can helps you a lot, and loading WordPress may be needed in some cases to run functions like dbDelta.

(There is no need to say to use a different db for tests, isn\'t it?)

Luckily PHPUnit allows you to organize your tests in "suites" that can be run separately, so you can write a suite for custom database tests where you load WordPress environment (or part of it) leaving all the rest of your tests WordPress-free.

Only be sure to write classes that abstract as many database operations as possible, in a way that all the other plugin classes make use of them, so that using mocks you can properly test the majority of classes without dealing with database.

For third time, writing code easily testable in isolation means writing better code.

结束

相关推荐

pingbacks testing

关于新wp安装(3.0.4)中PBs的功能测试,我有几个问题:发布帖子时是立即发送pingback,还是将其安排为cron作业?如果后者正确,作业多久运行一次,我可以手动触发它吗?除了将“尝试通知文章中链接到的任何博客”设置为“开”,当然还有帖子内容中指向另一个博客的链接之外,还有其他关于发送PBs的术语吗?(例如,发件人的帖子应该是公开的而不是私有的吗?博客应该是非私有的吗?)出站链接应该放在帖子内容中,还是可以放在帖子的自定义字段中,以便发送PB?如果我的博客中没有发送或接收PBs,那么调试和检测问题