我正在构建一个会话管理类和相关的单元测试。为了将类与 $_SESSION 的全局状态分开,我使用了一个非常简单的类来管理类和会话数据之间的绑定。
Binding类的整个源码如下:
class Binding implements iface\Binding
{
public function &getNamespace ($namespace)
{
return $_SESSION [$namespace];
}
}
在消费会话类中,我有以下内容:
protected function initStorage ()
{
// Check that storage hasn't already been bound to the session
if ($this -> storage === NULL)
{
// Attempt to start the session if it hasn't already been started
if (($this -> sessionId () !== '')
|| ((!$this -> headersSent ())
&& ($this -> startSession ())))
{
// Bind the storage to the session
$this -> storage =& $this -> binding -> getNamespace ($this -> namespace);
// Make sure the session is in a usable state
if (!$this -> hasData ())
{
$this -> reset ();
}
}
else
{
// We couldn't start the session
throw new \RuntimeException (__METHOD__ . ': Unable to initiate session storage at this time');
}
}
return $this;
}
sessionId、headersSent 和 startSession 是用作“测试接缝”的简单单行函数,我可以很容易地用 PHPUnit 中的模拟替换它们。
我在编写测试时意识到,我可以使用模拟绑定类做更多的事情,而不仅仅是将会话从类中分离出来,我还可以将其用作观察类的非公共属性的一种方式,而无需实际暴露任何内部状态,从而使班级变得脆弱。由于该类对数组的引用而不是直接对数组进行操作,因此我可以观察到被引用的数组。
我希望用 PHPUnit 的模拟 API 来做到这一点,但我不知道该怎么做。
我知道我可以创建一个返回这样的数组的模拟:
$mock = $this -> getMock ('iface\Binding');
$mock -> expects ($this -> any ())
-> method ('getNamespace')
-> will ($this -> returnValue (array ()));
但是,它对于观察状态变化没有用,因为它每次都返回一个不同的数组。我需要的是一个模拟,它每次都返回对同一个数组的引用。
最后我写了一个类来代替真正的 Binding 类并使用它:
class BindingMock implements iface\Binding
{
protected $storage = array ();
public function &getNamespace ($namespace)
{
return $this -> storage [$namespace];
}
}
使用此类可以让我在调用 Session API 之前和之后检查 $storage 的内容,因为我可以查看存储数组中的内容,而无需在 Session 类中公开非公共状态。这是一个使用该技术的示例测试:
public function testCreateItem ()
{
$storage =& $this -> binding -> getNamespace ('unittest');
$this -> assertEmpty ($storage);
$this -> object -> createItem ('This is a test', 'test');
$this -> assertNotEmpty ($storage);
$this -> assertEquals ('This is a test', $storage ['test']);
}
我宁愿能够使用 PHPUnit 生成替代类,但是,只为单元测试使用附加类似乎是错误的方法,除非在 PHPUnit 中无法实现相同的目标。