在PHPUnit中模拟/存根FTP操作

时间:2011-11-29 21:42:05

标签: 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;
}

更新/解决方案摘要

问题摘要

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

解决方案摘要

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

更新2

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

Mocking php global functions for unit testing

3 个答案:

答案 0 :(得分:10)

我想到了两种方法:

  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.'
        );
      }
      
    3. 在上面的测试用例中,setUp()注入模拟适配器,以便测试可以在不实际触发FTP连接的情况下调用FTP类“connect()方法”。然后,如果使用正确的参数调用适配器的connect()方法,则测试会为适配器播种仅返回 的结果。

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

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

    4. 另一种方法是使用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

答案 1 :(得分:4)

你的方法似乎是O-K。考虑到最终你将处于与外部直接交互的低级函数,你可以进行单元测试总是存在局限性。

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

答案 2 :(得分:3)

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

这些函数不是您编写的组件,因此不应对它们进行测试。

否则你可以开始为if甚至是+等运营商编写测试 - 但你没有。

再说一遍,看看你的代码会很好。如果你有程序样式代码,那么很难模拟/存根/欺骗每个FTP函数。实际上,无论如何这都不容易实现。

但是,如果您改为创建包装所有这些函数的FTP连接对象,则可以为该FTP连接对象创建测试重复。这需要重构您的代码。