对于组成另一个对象作为其实现的一部分的对象,编写单元测试的最佳方法是什么,只有主要对象才会被测试?琐碎的例子:
class myObj {
public function doSomethingWhichIsLogged()
{
// ...
$logger = new logger('/tmp/log.txt');
$logger->info('some message');
// ...
}
}
我知道可以设计对象以便可以注入logger对象依赖项并因此在单元测试中进行模拟,但情况并非总是如此 - 在更复杂的场景中,您确实需要编写其他对象或进行调用静态方法。
由于我们不想测试logger对象,只有myObj,我们如何进行?我们用测试脚本创建一个stubbed“double”吗?类似的东西:
class logger
{
public function __construct($filepath) {}
public function info($message) {}
}
class TestMyObj extends PHPUnit_Framework_TestCase
{
// ...
}
对于小对象来说这似乎是可行的,但对于更复杂的API而言,这将是一个痛苦,其中SUT依赖于返回值。另外,如果你想测试对依赖项对象的调用,你可以使用模拟对象吗?有没有办法模拟由SUT实例化的对象而不是传入?
我已经阅读了关于模拟的手册页,但它似乎并没有涵盖这种依赖是组合而不是聚合的情况。你是怎么做到的?
答案 0 :(得分:6)
关注 troelskn 建议这是你应该做的一个基本的例子。
<?php
class MyObj
{
/**
* @var LoggerInterface
*/
protected $_logger;
public function doSomethingWhichIsLogged()
{
// ...
$this->getLogger()->info('some message');
// ...
}
public function setLogger(LoggerInterface $logger)
{
$this->_logger = $logger;
}
public function getLogger()
{
return $this->_logger;
}
}
class MyObjText extends PHPUnit_Framework_TestCase
{
/**
* @var MyObj
*/
protected $_myObj;
public function setUp()
{
$this->_myObj = new MyObj;
}
public function testDoSomethingWhichIsLogged()
{
$mockedMethods = array('info');
$mock = $this->getMock('LoggerInterface', $mockedMethods);
$mock->expects($this->any())
->method('info')
->will($this->returnValue(null));
$this->_myObj->setLogger($mock);
// do your testing
}
}
有关模拟对象的更多信息,请访问in the manual。
答案 1 :(得分:4)
正如您似乎已经意识到的那样,Concrete Class Dependencies使得测试变得困难(或完全不可能)。您需要解除该依赖关系。一个不破坏现有API的简单更改是默认为当前行为,但提供一个钩子来覆盖它。有很多方法可以实现。
有些语言的工具可以将模拟类注入到代码中,但我不知道PHP的这类内容。在大多数情况下,无论如何,最好还是重构代码。
答案 2 :(得分:0)
看起来我误解了这个问题,让我再试一次:
你应该使用单件模式或工厂作为记录器,如果还不太晚:
class LoggerStub extends Logger {
public function info() {}
}
Logger::setInstance(new LoggerStub());
...
$logger = Logger::getInstance();
如果您无法更改代码,则可以使用重载{-3}}的全包类
class GenericStub {
public function __call($functionName, $arguments) {}
}
答案 3 :(得分:0)
实际上,构建PHPUnit的同一个人发布了PHP类重载的合理新扩展。它允许您在无法重构代码的情况下覆盖新运算符,遗憾的是,在Windows上安装并不是那么简单。