一般来说,我是一个相对较新的单元测试转换者,我在这里遇到了一个绊脚石:
如何使用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全局函数用于单元测试
我想到了两种方法:
为您的FTP类创建两个适配器:
- 使用PHP的ftp函数连接到远程服务器等的"真实"服务器。
一个"模拟",它实际上不连接任何东西,只返回种子数据。
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),并且可以说您现在已经引入了另一个必须为其编写测试的类。
另一种选择是使用PHPUnit的模拟框架在测试中创建动态模拟对象。您仍然需要注入一个适配器,但是您可以动态地创建它:
注意,上面的测试模拟了FTP类的适配器,而不是FTP类本身,因为那样做是相当愚蠢的。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.' ); }
这种方法比以前的方法有优点:
- 你没有创建任何新的类,PHPUnit的模拟框架有自己的测试覆盖,所以你不必为模拟类编写测试。
- 测试作为"引擎盖下"发生的事情的文档(尽管有些人可能认为这实际上不是一件好事)。
然而,这种方法也有一些缺点:
- 与之前的方法相比,它相当慢(在性能方面)。
- 您必须在使用mock的每个测试中编写大量额外的代码(尽管您可以重构常见操作以减轻其中的一些影响)。
你的方法似乎很好。考虑到最终您将在与外部直接交互的低级函数上进行单元测试,您所能进行的测试总是有限制的。
我建议您将FTP建立在可以模拟的适配器上,然后您可以通过集成测试覆盖实际的适配器测试。
虽然一个选项是模拟FTP服务器并在测试中连接它(您只需要更改FTP服务器的详细信息/连接配置到模拟服务器,而不更改任何代码),但还有另一个选项:您不需要对PHP的函数进行单元测试。
这些函数不是你编写的组件,所以你不应该测试它们。
否则你可以开始为if
甚至像+
这样的操作符编写测试-但是你没有。
更详细地说,看到你的代码会很好。如果您使用的是过程式代码,则很难模拟/存根/欺骗每个FTP函数。实际上,这在PHP中是不容易实现的。
但是如果您创建一个包含所有这些函数的FTP连接对象,您可以为该FTP连接对象创建一个测试副本。但是,这需要重构您的代码。