在PHPUnit中模拟/存根FTP操作


Mocking/stubbing FTP operations in PHPUnit

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

如何使用PHP的内置FTP功能测试连接到远程FTP服务器并执行操作的代码?在谷歌上搜索了一下,找到了一个Java的快速mock选项(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;
}

更新/解决方案总结

<

问题总结/em>

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

<

解决方案总结/em>

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

更新2

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

模拟php全局函数用于单元测试

我想到了两种方法:

  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的模拟框架有自己的测试覆盖,所以你不必为模拟类编写测试。
    • 测试作为"引擎盖下"发生的事情的文档(尽管有些人可能认为这实际上不是一件好事)。

    然而,这种方法也有一些缺点:

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

你的方法似乎很好。考虑到最终您将在与外部直接交互的低级函数上进行单元测试,您所能进行的测试总是有限制的。

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

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

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

否则你可以开始为if甚至像+这样的操作符编写测试-但是你没有。

更详细地说,看到你的代码会很好。如果您使用的是过程式代码,则很难模拟/存根/欺骗每个FTP函数。实际上,这在PHP中是不容易实现的。

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