Php 在单元测试中模拟正在测试的对象是否不好?
这是我正在进行单元测试的课程。目前我正在测试Php 在单元测试中模拟正在测试的对象是否不好?,php,unit-testing,phpunit,Php,Unit Testing,Phpunit,这是我正在进行单元测试的课程。目前我正在测试doSomething功能: class FooClass { public function doSomething( $user ) { $conn = $this->getUniqueConnection( $user->id ); $conn->doSomethingDestructive(); } private function getUniqueConnection( $id ) {
doSomething
功能:
class FooClass {
public function doSomething( $user ) {
$conn = $this->getUniqueConnection( $user->id );
$conn->doSomethingDestructive();
}
private function getUniqueConnection( $id ) {
return new UniqueConnection( $id );
}
}
如您所见,doSomething
函数根据它接收的参数的属性获取UniqueConnection
(我在这里不测试的一个类)的新实例。问题是,UniqueConnection::doSomethingDestructive
方法是我在测试期间无法调用的,因为它。。。破坏性。因此,我想存根/模拟UniqueConnection
,而不是使用真正的连接
我看不到任何方法来注入我的模拟UniqueConnection
。我会将UniqueConnection
作为FooClass
的构造函数参数,但是,正如您所看到的,基于doSomething
函数的参数会创建一个新的参数,并且它可能被调用的所有唯一ID都不会提前知道
我能看到的唯一选择是测试
FooClass
的模拟,而不是FooClass
本身。然后我将用返回mock/stub的函数替换getUniqueConnection
函数。这似乎不适合测试一个mock,但我看不到任何方法来实现我所追求的其他目标UniqueConnection
是第三方供应商库,无法修改 您可以创建一个UniqueConnectionFactory
,并将其实例传递给FooClass。那你有
private function getUniqueConnection( $id ) {
return $this->uniqueConnectionFactory->create( $id );
}
一般来说,这是使用工厂的好处之一-您可以将
新的操作符保留在类之外,这使您可以更轻松地更改正在创建的对象。在某些情况下,以支持不同执行模式的方式创建类非常重要。其中一个案例就是你所要求的
创建支持各种模式的类。比如说
Class Connection {
private $mode;
public function setMode($mode) {
$this -> $mode = $mode;
}
}
现在,您的doSomethingDestructive
可以按照执行模式进行操作
下次,当您测试类时,您不必担心破坏性函数会意外地执行某些破坏操作
public function doSomething( $user ) {
$conn = $this->getUniqueConnection( $user->id );
$conn -> setMode("test"); //Now we are safe
$conn->doSomethingDestructive(); //But the Testing is still being Ran
}
在这种情况下,您需要的不是一个模拟对象,而是一个测试子类。破坏你的$conn->doSomethingDestructive()将>编码到一个方法中,然后将FooClass
子类化为TestFooClass
,并重写子类中的新方法。然后,您可以使用子类进行测试,而不会得到不需要的破坏性行为
例如:
class FooClass {
public function doSomething( $user ) {
$conn = $this->getUniqueConnection( $user->id );
$this->connDoSomethingDestructive($conn);
}
protected function connDoSomethingDestructive($conn) {
$conn->doSomethingDestructive();
}
private function getUniqueConnection( $id ) {
return new UniqueConnection( $id );
}
}
class TestFooClass extends FooClass {
protected function connDoSomethingDestructive() {
}
private function getUniqueConnection( $id ) {
return new MockUniqueConnection( $id );
}
}
在按照rambo coder的建议花时间重构代码以使用工厂之前,您可以使用部分模拟来返回非破坏性的唯一连接。当你发现自己处于这个位置时,通常意味着被测试的类有不止一个职责
function testSomething() {
$mockConn = $this->getMock('UniqueConnection');
$mockConn->expects($this->once())
->method('doSomethingDestructive')
->will(...);
$mockFoo = $this->getMock('FooClass', array('getUniqueConnection'));
$mockFoo->expects($this->once())
->method('getUniqueConnection')
->will($this->returnValue($mockConn));
$mockFoo->doSomething();
}
正如Rambo Coder所说,这是一个在课堂上做得太多的问题。我甚至不想创建工厂,特别是如果您只创建一个特定类的实例。最简单的解决方案是将创建UniqueConnection的责任倒置:
<?php
class FooClass {
public function doSomething( UniqueConnection $connection ) {
$connection->doSomethingDestructive( );
}
}
我会避免将测试行为放在原始类中。相反,创建一个具有测试行为的测试类。@ThomasW,创建一个仅用于测试的不同类,并在测试阶段完成后将其删除,这也是一种可行的方法。但创建对象的不同执行模式也是一个很好的选择。如果对象支持不同的模式,如“调试”、“测试”、“只读”,那么它在迭代开发、原型设计和敏捷方法学的阶段变得非常方便。使用不同的执行模式的缺点是必须测试模式切换代码。在测试生产模式时,您仍然会遇到最初的问题。@DavidHarkness,切换模式通常在应用程序域或根类中全局发生。这只是一个需要测试的案例。此外,我提供的解决方案首先消除了对模拟对象的需要,只在测试模式下执行测试友好的代码。@Starx-设置模式的代码位于一个位置,但检查该模式并根据其值执行X或Y的代码散布在整个生产代码中。问题不在于X和Y之间的选择,而是单元测试只执行其中一个分支。将$conn->doSomethingDestructive()分解为一个方法是什么意思?所谓“分解为一个方法”,我的意思是创建一个新方法来处理该功能。我已经更新了我的答案来说明我的意思。虽然这样做有效,但使用返回非破坏性连接的部分模拟似乎更容易。除了可能有多个负责人之外,部分模拟的复杂性增加了,真的是一个更好的选择吗?@Starx-比修改生产代码并让它半途而废要好吗?对它看起来很复杂,因为模拟API有点冗长。
<?php
class FooClass {
public function doSomething( UniqueConnection $connection ) {
$connection->doSomethingDestructive( );
}
}