PHPUnit 测试是否调用了 class 方法

PHPUnit test if class methods were called

我有模型 class,它在其中一种方法中调用邮件程序 class:

class someModel{
    public function sendEmail($data){
         $mailer = new Mailer();
         $mailer->setFrom($data['from']);
         $mailer->setTo($data['to']);
         $mailer->setSubject($data['subject']);
         return $mailer->send();
    }
}

如何测试 sendEmail 方法?也许我应该模拟邮件程序 class 并检查是否在 sendMail 方法中调用了所有这些邮件程序方法?

您的帮助将不胜感激。

我会(并且在我自己的代码中使用 Mailer!)将您的 Mailer 实例包装在您编写的 class 中。换句话说,制作您自己的电子邮件 class,在后台使用 Mailer。这使您可以将 Mailer 的界面简化为您所需要的,并且更容易模拟它。它还使您能够在以后无缝地替换 Mailer。

当您包装 classes 以隐藏外部依赖项时,要记住的最重要的事情是保持包装器 class 简单。它的唯一目的是让您换出电子邮件库class,不提供任何复杂的逻辑。

示例:

class Emailer {
    private $mailer = new Mailer();

    public function send($to, $from, $subject, $data) {
         $this->mailer->setFrom($from);
         $this->mailer->setTo($to);
         ...
         return $mailer->send();
    }
}

class EmailerMock extends Emailer {
    public function send($to, $from, $subject, $data) {
         ... Store whatever test data you want to verify ...
    }

    //Accessors for testing the right data was sent in your unit test
    public function getTo() { ... }
    ...
}

我对所有 classes/libraries 想要接触我的软件外部内容的人都遵循相同的模式。其他好的候选者是数据库连接、Web 服务连接、缓存连接等。

编辑: gontrollez 在他关于依赖注入的回答中提出了一个很好的观点。我没有明确提及它,但在创建包装器之后,您可能希望使用某种形式的依赖注入将其放入要使用它的代码中。传入实例可以使用 Mocked 实例设置测试用例。

执行此操作的一种方法是按照 gontrollez 的建议将实例传递给构造函数。在很多情况下,这是最好的方法。但是,对于我正在模拟的 "external services",我发现该方法变得乏味,因为很多 classes 最终都需要传入实例。例如,考虑一个要 Mock 用于测试的数据库驱动程序,但是你在许多不同的 classes 中使用。因此,我所做的是创建一个单例 class,并使用一种方法让我一次模拟整个事情。然后任何客户端代码都可以只使用单例来访问服务而不知道它被模拟了。它看起来像这样:

class Externals {
    static private $instance = null;
    private $db = null;
    private $email = null;
    ...

    private function __construct() {
        $this->db = new RealDB();
        $this->mail = new RealMail();
    }

    static function initTest() {
        self::get();         //Ensure instance created
        $db = new MockDB();
        $email = new MockEmail();
    }

    static function get() {
        if(!self::$instance)
            self::$instance = new Externals();
        return self::$instance;
    }

    function getDB() { return $this->db; }
    function getMail() { return $this->mail; }
    ....
}

然后您可以使用 phpunit 的 bootstrap 文件功能来调用 Externals::initTest() 并且您的所有测试都将使用模拟的外部设置!

首先,正如 RyanW 所说,您应该为 Mailer 编写自己的包装器。

其次,要对其进行测试,请使用模拟:

<?php

class someModelTest extends \PHPUnit_Framework_TestCase
{
    public function testSendEmail()
    {
        // Mock the class so we can verify that the methods are called
        $model = $this->getMock('someModel', array('setFrom', 'setTo', 'setSubject', 'send'));
        $controller->expects($this->once())
            ->method('setFrom');
        $controller->expects($this->once())
            ->method('setTo');
        $controller->expects($this->once())
            ->method('setSubject');
        $controller->expects($this->once())
            ->method('send');
        $model->sendEmail();
    }
}

以上代码未经测试,但它基本上模拟了 someModel class,为 sendEmail 中调用的每个函数创建虚拟函数。然后它会测试以确保 sendEmail 调用的每个函数在调用 sendEmail 时恰好被调用一次。

See the PHPUnit docs 有关模拟的更多信息。

IMO 包装 Mailer class 并不能解决您面临的问题,即您无法控制正在使用的 Mail 实例。

问题来自于在需要它们的对象内部创建依赖项,而不是像这样从外部注入它们:

class someModel{

  private $mailer;

  public function __construct(Mailer $mailer) {
       $this->mailer = $mailer;
  }

  public function sendEmail($data){         
     $this->mailer->setFrom($data['from']);
     $this->mailer->setTo($data['to']);
     $this->mailer->setSubject($data['subject']);
     return $this->mailer->send();
  }
}

创建someModel实例时,必须传递一个Mail实例(这是一个外部依赖)。 在测试中,您可以传递邮件模拟,它将检查是否进行了正确的调用。


选择:

如果您觉得注入 Mail 实例很糟糕(可能是因为有很多 someModel 实例),或者您不能以这种方式更改代码,那么您可以使用服务存储库,它将保留一个单个 Mail 实例并允许您在外部设置它(同样,在测试中您将设置一个模拟)。

尝试一个简单的 Pimple