【问题标题】:Mocking/stubbing FTP operations in PHPUnit在 PHPUnit 中模拟/存根 FTP 操作
【发布时间】:2012-01-09 05:28:06
【问题描述】:

我是一个相对较新的单元测试转换者,我在这里遇到了一个绊脚石:

如何使用 PHP 的内置 ftp 函数测试连接到远程 FTP 服务器并在其上执行操作的代码?一些谷歌搜索为 Java (MockFtpServer) 提供了一个快速模拟选项,但 PHP 没有现成的可用选项。

我怀疑答案可能是为 PHP 的 ftp 函数创建一个包装类,该类随后可以被存根/模拟以模仿成功/不成功的 ftp 操作,但我真的很感谢比我更聪明的人的一些意见!

请注意,我一直在使用 PHPUnit,并且需要专门针对该框架的帮助。


根据@hakre 的请求,我想测试的简化代码如下所示。我本质上是在问最好的测试方法:

public function connect($conn_name, $opt=array())
{
  if ($this->ping($conn_name)) {
    return TRUE;
  }

  $r = FALSE;

  try {    
    if ($this->conns[$conn_name] = ftp_connect($opt['host'])) {
      ftp_login($this->conns[$conn_name], $opt['user'], $opt['pass']);
    }
    $r = TRUE;
  } catch(FtpException $e) {
    // there was a problem with the ftp operation and the
    // custom error handler threw an exception
  }

  return $r;
}

更新/解决方案摘要

问题总结

我不确定如何单独测试需要与远程 FTP 服务器通信的方法。您应该如何测试是否能够连接到您无法控制的外部资源,对吧?

解决方案总结

为 FTP 操作创建一个适配器类(方法:连接、ping 等)。然后,在测试使用适配器执行 FTP 操作的其他代码时,该适配器类很容易被存根以返回特定值。

更新 2

我最近在您的测试中发现了一个使用名称空间的绝妙技巧,它允许您“模拟”PHP 的内置函数。虽然适配器在我的特定情况下是正确的方法,但在类似情况下这可能对其他人有所帮助:

Mocking php global functions for unit testing

【问题讨论】:

    标签: php phpunit


    【解决方案1】:

    想到的两种方法:

    1. 为您的 FTP 类创建两个适配器:

      1. “真正的”使用 PHP 的 ftp 函数连接到远程服务器等。
      2. 一种“模拟”,实际上并不连接任何东西,只返回种子数据。

        FTP 类的connect() 方法如下所示:

        public function connect($name, $opt=array())
        {
          return $this->getAdapter()->connect($name, $opt);
        }
        

        模拟适配器可能如下所示:

        class FTPMockAdapter
          implements IFTPAdapter
        {
          protected $_seeded = array();
        
          public function connect($name, $opt=array())
          {
            return $this->_seeded['connect'][serialize(compact('name', 'opt'))];
          }
        
          public function seed($data, $method, $opt)
          {
            $this->_seeded[$method][serialize($opt)] = $data;
          }
        }
        

        在您的测试中,您将使用结果播种适配器并验证 connect() 是否被正确调用:

        public function setUp(  )
        {
          $this->_adapter = new FTPMockAdapter();
          $this->_fixture->setAdapter($this->_adapter);
        }
        
        /** This test is worthless for testing the FTP class, as it
         *    basically only tests the mock adapter, but hopefully
         *    it at least illustrates the idea at work.
         */
        public function testConnect(  )
        {
          $name    = '...';
          $opt     = array(...);
          $success = true
        
          // Seed the connection response to the adapter.
          $this->_adapter->seed($success, 'connect', compact('name', 'opt'));
        
          // Invoke the fixture's connect() method and make sure it invokes the
          //  adapter properly.
          $this->assertEquals($success, $this->_fixture->connect($name, $opt),
            'Expected connect() to connect to correct server.'
          );
        }
        

      在上面的测试用例中,setUp() 注入了模拟适配器,以便测试可以调用 FTP 类的connect() 方法,而无需实际触发 FTP 连接。然后,测试为适配器播种结果,在适配器的 connect() 方法被调用时使用正确的参数。

      此方法的优点是您可以自定义模拟对象的行为,如果您需要在多个测试用例中使用模拟,它会将所有代码保存在一个位置。

      这种方法的缺点是您必须复制许多已经构建的功能(请参阅方法 #2),并且可以说您现在引入了另一个必须为其编写测试的类。

    2. 另一种方法是使用 PHPUnit 的模拟框架在您的测试中创建动态模拟对象。您仍然需要注入适配器,但您可以即时创建它:

      public function setUp(  )
      {
        $this->_adapter = $this->getMock('FTPAdapter');
        $this->_fixture->setAdapter($this->_adapter);
      }
      
      public function testConnect(  )
      {
        $name = '...';
        $opt  = array(...);
      
        $this->_adapter
          ->expects($this->once())
          ->method('connect')
          ->with($this->equalTo($name), $this->equalTo($opt))
          ->will($this->returnValue(true));
      
        $this->assertTrue($this->_fixture->connect($name, $opt),
          'Expected connect() to connect to correct server.'
        );
      }
      

      请注意,上面的测试模拟了 FTP 类的 适配器而不是 FTP 类本身,因为这样做是一件相当愚蠢的事情。

      这种方法比以前的方法有优势:

      • 您没有创建任何新类,PHPUnit 的模拟框架有自己的测试覆盖范围,因此您不必为模拟类编写测试。
      • 测试充当“幕后”发生的事情的文档(尽管有些人可能认为这实际上不是一件好事)。

      不过,这种方法也有一些缺点:

      • 与以前的方法相比,它相当慢(就性能而言)。
      • 您必须在每个使用模拟的测试中编写大量额外代码(尽管您可以重构常用操作来缓解其中的一些问题)。

      更多信息请参见http://phpunit.de/manual/current/en/test-doubles.html#test-doubles.mock-objects

    【讨论】:

    • 感谢您提供详尽的、针对 PHPUnit 的建议。
    • 另外,一旦我理解了您的示例,就会清楚真正的困难在于我的代码设计不佳。在使用 ftp 工作的适配器类更好地分离关注点之后,您布置的路径很清楚。再次感谢!
    【解决方案2】:

    您的方法似乎没问题。考虑到最终您将处于与外部直接交互的低级功能,您可以单元测试的内容总是存在限制。

    我建议您将 FTP 建立在您可以模拟的适配器上,然后您可以通过集成测试涵盖实际的适配器测试。

    【讨论】:

    • 谢谢——这正是我想要的。我基本上想知道如何测试我的代码在 ftp 操作失败或成功时是否正确,而不是 ftp 操作自己失败或成功。
    【解决方案3】:

    虽然一种选择是模拟 FTP 服务器并在测试中连接到它(您只需将 ftp-server 详细信息/连接配置更改为模拟服务器而不更改任何代码)还有另一种选择:您不需要对 PHP 的函数进行单元测试。

    那些函数不是你写的组件,所以你不应该测试它们。

    否则,您可以开始为 if 甚至像 + 这样的运算符编写测试 - 但您不这样做。

    多说,看看你的代码就好了。如果你有程序风格的代码,就很难模拟/存根/复制每个 FTP 函数。其实这对 PHP 来说是不容易的。

    但是,如果您改为创建一个包含所有这些功能的 FTP 连接对象,则可以为该 FTP 连接对象创建一个测试副本。然而,这需要重构您的代码。

    【讨论】:

    • 等等,你的意思是我的单元测试可以确保双引号字符串扩展其中的变量吗?各位大神! :)
    • 是的,另一个要测试的字段。但不要这样做。如果有兴趣支持 PHP,可以在上游项目中为 PHP 编写测试,见:qa.php.net
    猜你喜欢
    • 2018-02-08
    • 2011-12-26
    • 1970-01-01
    • 2015-03-23
    • 1970-01-01
    • 1970-01-01
    • 2012-05-23
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多