我是一个相对较新的单元测试转换器,我遇到了一个绊脚石:
如何使用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的内置函数。虽然适配器是我的特定情况的正确方法,但这可能对类似情况下的其他人有所帮助:
答案 0 :(得分:10)
我想到了两种方法:
为您的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连接的情况下调用FTP类“connect()
方法”。然后,如果使用正确的参数调用适配器的connect()
方法,则测试会为适配器播种仅返回 的结果。
此方法的优点是您可以自定义模拟对象的行为,并且如果您需要在多个测试用例中使用模拟,它会将所有代码保存在一个位置。
这种方法的缺点是你必须复制已经构建的许多功能(参见方法#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类本身,因为这将是一件相当愚蠢的事情。
这种方法优于以前的方法:
然而,这种方法有一些缺点:
有关详细信息,请参阅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连接对象创建测试重复。这需要重构您的代码。