为什么在像请求驱动的PHP这样的语言中测试singletons或注册表模式很难?
您可以编写和运行除实际程序执行之外的测试,这样您就可以自由地影响程序的全局状态,并为每个测试函数运行一些拆卸和初始化,以使每个测试的状态达到相同状态
我错过了什么吗?
答案 0 :(得分:35)
虽然“确实可以编写并运行除实际程序执行之外的测试,这样您就可以自由地影响程序的全局状态,并为每个测试函数运行一些拆卸和初始化来获取它每次测试都达到相同的状态。“,这样做很乏味。您希望单独测试TestSubject,而不是花时间重新创建工作环境。
class MyTestSubject
{
protected $registry;
public function __construct()
{
$this->registry = Registry::getInstance();
}
public function foo($id)
{
return $this->doSomethingWithResults(
$registry->get('MyActiveRecord')->findById($id)
);
}
}
要实现这一点,你必须拥有具体的Registry
。它是硬编码的,它是一个单身人士。后者意味着防止先前测试的任何副作用。必须为将在MyTestSubject上运行的每个测试重置它。你可以添加一个Registry::reset()
方法并在setup()
中调用它,但添加一个方法只是为了能够测试看起来很难看。我们假设您无论如何都需要这种方法,所以最终会得到
public function setup()
{
Registry::reset();
$this->testSubject = new MyTestSubject;
}
现在你仍然没有'{A 1}}中应该返回的'MyActiveRecord'对象。因为你喜欢Registry,你的MyActiveRecord实际上看起来像这样
foo
在MyActiveRecord的构造函数中还有另一个对Registry的调用。您测试必须确保它包含某些内容,否则测试将失败。当然,我们的数据库类也是Singleton,需要在测试之间重置。卫生署!
class MyActiveRecord
{
protected $db;
public function __construct()
{
$registry = Registry::getInstance();
$this->db = $registry->get('db');
}
public function findById($id) { … }
}
所以最终设置完成后,你可以进行测试
public function setup()
{
Registry::reset();
Db::reset();
Registry::set('db', Db::getInstance('host', 'user', 'pass', 'db'));
Registry::set('MyActiveRecord', new MyActiveRecord);
$this->testSubject = new MyTestSubject;
}
并意识到您还有另一个依赖项:您的物理测试数据库尚未设置。在您设置测试数据库并填充数据时,您的老板出现并告诉您现在正在SOA,所有这些数据库调用都必须替换为Web service次调用。
有一个新类public function testFooDoesSomethingToQueryResults()
{
$this->assertSame('expectedResult', $this->testSubject->findById(1));
}
,你必须让MyActiveRecord使用它。很棒,正是你需要的。现在您必须更改使用数据库的所有测试。你想,该死的。所有这些废话只是为了确保MyWebService
按预期工作? doSomethingWithResults
并不关心数据的来源。
好消息是,您确实可以通过存根或mock来替换所有依赖项。测试双重将假装是真实的。
MyTestSubject
这将为测试期间期望 期望 一次 创建一个双倍的第一个参数方法 $mock = $this->getMock('MyWebservice');
$mock->expects($this->once())
->method('findById')
->with($this->equalTo(1))
->will($this->returnValue('Expected Unprocessed Data'));
为1. 将返回预定义数据。
将其放入TestCase中的方法后,findById
变为
setup
大。您现在不再需要为创建真实环境而烦恼。好吧,除了注册表。怎么样嘲笑那个。但是如何做到这一点。它是硬编码的,因此在测试运行时无法替换。掷骰子!
但等一下,我们不是只说MyTestClass不关心数据来自何处?是的,它只关心它可以调用public function setup()
{
Registry::reset();
Registry::set('MyWebservice', $this->getWebserviceMock());
$this->testSubject = new MyTestSubject;
}
方法。你希望现在想一想:为什么注册表会在那里?对,你是对的。让我们将整个事情改为
findById
Byebye Registry。我们现在正在注入依赖MyWebSe ...错误... Finder ?!是啊。我们只关心方法class MyTestSubject
{
protected $finder;
public function __construct(Finder $finder)
{
$this->finder = $finder;
}
public function foo($id)
{
return $this->doSomethingWithResults(
$this->finder->findById($id)
);
}
}
,所以我们现在使用的是接口
findById
不要忘记相应地更改模拟
interface Finder
{
public function findById($id);
}
和setup()变为
$mock = $this->getMock('Finder');
$mock->expects($this->once())
->method('findById')
->with($this->equalTo(1))
->will($this->returnValue('Expected Unprocessed Data'));
瞧!很好,很容易。我们现在可以集中精力测试MyTestClass。
当你这样做的时候,你的老板再次打电话说他希望你切换回数据库,因为SOA实际上只是价格过高的顾问使用的流行语,让你觉得自己很有事。这次你不用担心,因为你不必再次改变你的测试。他们不再依赖环境。
当然,您仍然需要确保MyWebservice和MyActiveRecord都为您的实际代码实现Finder接口,但由于我们假设它们已经拥有这些方法,所以只需要打{{1}}上课。
就是这样。希望有所帮助。
在
中测试单身人士和处理全局状态时,您可以找到有关其他缺点的其他信息这应该是最感兴趣的,因为它是PHPUnit的作者,并解释了PHPUnit中实际示例的困难。
同样感兴趣的是:
答案 1 :(得分:5)
Singletons(在所有OOP语言中,而不仅仅是PHP)使得一种称为单元测试的特殊调试很困难,原因与全局变量相同。他们将全局状态引入程序,这意味着您无法单独测试依赖于单例的软件模块。单元测试应该只包括被测代码(及其超类)。
单身人士本质上是全球性的,虽然在某些情况下拥有全球国家可能有意义,但除非必要,否则应该避免。
答案 2 :(得分:2)
完成PHP测试后,您可以像这样刷新单例实例:
protected function tearDown()
{
$reflection = new ReflectionClass('MySingleton');
$property = $reflection->getProperty("_instance");
$property->setAccessible(true);
$property->setValue(null);
}