在PHPUnit中模拟/存根FTP操作

rdl*_*rey 25 php phpunit

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

如何使用PHP的内置ftp函数测试连接到远程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;
}
Run Code Online (Sandbox Code Playgroud)

更新/解决方案摘要

问题摘要

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

解决方案摘要

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

更新2

我最近在测试中遇到了一个使用命名空间的漂亮技巧,它允许你"模拟"PHP的内置函数.虽然适配器是我的特定情况下的正确方法,但这可能对类似情况下的其他人有所帮助:

模拟php全局函数进行单元测试

小智 10

想到两种方法:

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

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

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

      public function connect($name, $opt=array())
      {
        return $this->getAdapter()->connect($name, $opt);
      }
      
      Run Code Online (Sandbox Code Playgroud)

      模拟适配器可能看起来像这样:

      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;
        }
      }
      
      Run Code Online (Sandbox Code Playgroud)

      在您的测试中,您将使用结果为适配器播种并验证是否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.'
        );
      }
      
      Run Code Online (Sandbox Code Playgroud)

    在上面的测试用例中,setUp()注入模拟适配器,以便测试可以在connect()不实际触发FTP连接的情况下调用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.'
      );
    }
    
    Run Code Online (Sandbox Code Playgroud)

    请注意,上面的测试模拟FTP类的适配器,而不是FTP类本身,因为这将是一个相当愚蠢的事情.

    这种方法优于以前的方法:

    • 您没有创建任何新类,并且PHPUnit的模拟框架有自己的测试覆盖率,因此您不必为mock类编写测试.
    • 该测试充当了"引擎盖下"正在发生的事情的文档(虽然有些人可能认为这实际上并不是一件好事).

    但是,这种方法有一些缺点:

    • 与之前的方法相比,它(在性能方面)相当缓慢.
    • 你必须在每个使用mock的测试中编写很多额外的代码(尽管你可以重构常见的操作来减轻其中的一些).

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

  • 感谢彻底的PHPUnit特定建议. (2认同)
  • 此外,一旦我理解了你的例子,很明显真正的困难来自我的代码的潜在的糟糕设计.在更好地将问题与ftp工作的适配器类分离之后,您所布置的路径是清楚的.再次感谢! (2认同)