单元测试-处理依赖项

时间:2016-07-25 作者:Andrea Sciamanna

这可能被视为Testing hooks callback.

问题:我想测试一个创建My_Notice 在插件外部定义的类(我们称之为“主插件”)。

我的单元测试对My_Notice 因为它是在第三方库(确切地说是另一个插件)中定义的。因此,我有以下选择(据我所知):

存根My_Notice 类:很难维护,包括来自第三方库的所需文件:这可能可行,但我正在减少测试的孤立性,动态地存根类:不确定这是否可行,但这与模拟类非常相似,只是它还应该创建相同的定义,以便我正在测试的类能够实例化它The answer from @gmazzap 指出我们应该避免创建这种依赖关系,这是我完全同意的。创建存根类的想法对我来说并不好:我宁愿包含“主插件”的代码,并承担所有后果)。

然而,我不知道我该怎么做。

下面是我尝试测试的代码示例:

class My_Admin_Notices_Handler {
    public function __construct( My_Notices $admin_notices ) {
        $this->admin_notices = $admin_notices;
    }

    /**
     * This will be hooked to the `activated_plugin` action
     *
     * @param string $plugin
     * @param bool   $network_wide
     */
    public function activated_plugin( $plugin, $network_wide ) {
        $this->add_notice( \'plugin\', \'activated\', $plugin );
    }

    /**
     * @param string $type
     * @param string $action
     * @param string $plugin
     */
    private function add_notice( $type, $action, $plugin ) {
        $message = \'\';
        if ( \'activated\' === $action ) {
            if ( \'plugin\' === $type ) {
                $message = __( \'%1s Some message for plugin(s)\', \'my-test-domain\' );
            }
            if ( \'theme\' === $type ) {
                $message = __( \'%1s Some message for the theme\', \'my-test-domain\' );
            }
        }
        if ( \'updated\' === $action && ( \'plugin\' === $type || \'theme\' === $type ) ) {
            $message = __( \'%1s Another message for updated theme or plugin(s)\', \'my-test-domain\' );
        }

        if ( $message ) {
            $notice          = new My_Notice( $plugin, \'wpml-st-string-scan\' );
            $notice->text    = $message;
            $notice->actions = array(
                new My_Admin_Notice_Action( __( \'Scan now\', \'my-test-domain\' ), \'#\' ),
                new My_Admin_Notice_Action( __( \'Skip\', \'my-test-domain\' ), \'#\', true ),
            );
            $this->admin_notices->add_notice( $notice );
        }
    }
}
基本上,这个类有一个方法activated_plugin. 此方法构建一个“notification”类的实例,该实例将存储在My_Notices 实例传递给构造函数。

的构造函数My_Notice 类接收两个基本参数(UID和“group”),并设置一些属性(请注意My_Admin_Notice_Action 类)。

我怎样才能My_Notice 为注入的依赖项分类?

当然,我可以使用一个关联数组,调用一些动作,这些动作被“主插件”钩住,并在类的参数中转换该数组,但在我看来它并不干净。

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

Is object injection really necessary?

To be fully testable in isolation, code should avoid to instantiate classes directly, and inject any object that is needed inside objects.

However, there are at least two exceptions to this rule:

  1. The class is a language class, e.g. ArrayObject
  2. The class is a proper "value object".

No need to force injection for core objects

The first case is easy to explain: it is something embedded in the language so you just assume it works. Otherwise you should also tests any PHP core functions or statement like return

No need to force injection for value object

The second case, is related to the nature of a value object. In facts:

  • it is immutable
  • it has no any possible polymorphic alternative
  • by definition, a value object instance is indistinguishable from another that has same constructor arguments

It means that a value object can be seen as an immutable type on its own, just like, for example, a string.

If some code does $myEmail = \'[email protected]\' no one is concerned about mocking that string, and in same way, no one should be concerned about mocking a line like new Email(\'[email protected]\') (assuming that Email is immutable and possibly final).

For what I can guess from your code, My_Admin_Notice_Action is a good candidate to be / become a value object. But, can\'t be sure without seeing the code.

I can\'t say the same of My_Notice, but that\'s just another guess.

If inject is necessary…

In case the class that is instantiated in another class is not one of the two cases above, that is surely better to inject it.

However, like example in OP, the class to be constructed needs arguments that depends on context.

There\'s no "one" answer in this case, but different approaches that may be all valid depending on the case.

Instantiation in client code

A simple approach is to separate "objects code" from "client code". Where client code is the code that makes use of objects.

In this way you could test objects code using unit tests, and leave client code testing to functional / integration tests, where you don\'t have to worry about isoltation.

In your case it will be something like:

add_action( \'activate_plugin\', function( $plugin, $network_wide ) {

   $message = My_Admin_Notices_Message( \'plugin\', \'activated\' );

   if ( $message->has_text() ) {

      $notice = new My_Notice( $plugin, $message, \'wpml-st-string-scan\' );
      $notice->add_action( new My_Admin_Notice_Action( \'Scan now\', \'#\' ) );
      $notice->add_action( new My_Admin_Notice_Action( \'Skip\', \'#\', true ) );

      $notices = new My_Notices();
      $notices->add_notice( $notice );

      $handler = new My_Admin_Notices_Handler( $notices );
      $handler->handle_notices();
   }

}, 10, 2);

I made some guesses on your code and write methods and classes that may not exists (like My_Admin_Notices_Message), but the point here is that the closure above contains all client code needed to instantiate and "use" the objects. You can then test your objects in isolation because no one of those objects needs to instantiate other objects, but they all receive necessary instances in constructor or as methods params.

Simplify client code with factories

The approach above may work well for small plugins (or small part of a plugin that can be isolated from the rest of it), but for bigger code bases, using only that approach you may end in big client code in closures which, among other things, is very hard to test and maintain.

In those cases, factories may help you. Factories are objects with the sole scope of creting other objects. Most of the time is good to have specific factories for object of the same type (implementing same interface).

With factories, the code above might look like this:

add_action( \'activate_plugin\', function( $plugin, $network_wide ) {

   $notice = $notice_factory->create_for( \'plugin\', \'activated\' );

   if ( $notice instanceof My_Notice_Interface ) {
      $handler_factory = new My_Admin_Notices_Handler_Factory();
      $handler = $handler_factory->build_for_notices( [ $notice ] );
      $handler->handle_notices();
   }

}, 10, 2);

All the instantiation code is in factories. You can still test factories in isolation, because you need to test that given proper arguments they produce expected classes (or that given wrong arguments they produce expected errors).

And still you can test all the other objects in isolation because no objects needs to create instances, in facts instantiation code is all in factories.

Of course, remember that value objects don\'t need factories... it would be like create factories for strings...

What if I can\'t change the code?

Stubs

Sometimes it is not possible to change the code that instantiates other objects, for different reasons. E.g. code is 3rd party, backward compatiblity, and so on.

In those cases, if is possible to run the tests without loading the classes being instantiated, then you can write some stubs.

Let\'s assume you have a code that does:

class Foo {

   public function run_something() {
     $something = new Something();
     $something->run();
   }
}

If you are able to run tests without loading the Something class, you can write a custom Something class just for the purpose of testing (a "stub").

It is always better to keep stubs very simple, e.g.:

class Something{

   public function run() {
     return \'I ran\'.
   }
}

When tests run, you can then load the file that contains this stub for Something class (e.g. from tests setUp()) and when the class under tests will instantiate a new Something, you will tests it in isolation, since the setup is very simple and you can create it in a way that by design it does what you expect.

Of course this is not very simple to maintain, but considering that normally you don\'t unit tests 3rd party code, rarely you need to do this.

Sometimes, though, this is helpful for testing in isolation plugins / themes code that instantiate WordPress objects (e.g. WP_Post).

Mockery overload

Using Mockery (a library to that provides tools for PHP unit tests) you can even avoid to write those stubs. With Mockery "instance mock" (aka "overload") is possible to intercept new instances creation and replace with a mock. This article explains pretty well how to do it.

When class is loaded...

If the code to test has hard dependencies (instantiate classes using new) and there\'s no possibility to load tests without loading the class that is going to be instantiated there\'re very small chances to test it in isolation without touching the code (or writing an abstraction layer around it).

However, note that test bootstrap file is often loaded as absolute first thing. So there are, at least, two cases in you can force the loading of your stubs:

  1. Code uses an autoloader. In this case, if you load the stubs before loading the autoloader, then the "real" class is never loaded, because when new is used, the class is already found and autoloader not triggered.

  2. Code checks for class_exists before defining / loading the class. In that case to load the stubs as first thing, will prevent the "real" class to be loaded.

The tricky last resort

When everything else fails, there\'s another thing you could do to test hard dependencies.

Very often hard dependencies are stored as private variables.

class Foo {

   public function __construct() {
     $this->something = new Something();
   }

   public function run_something() {
     return $this->something->run();
   }
}

in cases like this, you could replace the hard dependencies with a mock / stub after the instance is created.

This because even private properties can be (quite) easily replaced in PHP. In more than one way, actually.

Without digging into details, I can say that closure binding can be used to do that. I even wrote a library called "Andrew" that can be used for the scope.

Using Andrew (and Mockery) to test the class Foo above, you can do:

public function test_run_something() {

    $foo = new Foo();

    $something_mock = Mockery::mock( \'Something\' );
    $something_mock
        ->shouldReceive(\'run\')
        ->once()
        ->withNoArgs()
        ->andReturn(\'I ran.\');

    // changing proxy properties will change private properties of proxied object
    $proxy = new Andrew\\Proxy( $foo );
    $proxy->something = $something_mock;

    $this->assertSame( \'I ran.\', $foo->run_something() );
}

相关推荐

pingbacks testing

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