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( );
  }
}