在单元测试中模拟正在测试的对象是否不好


Is it bad to mock the object being tested in a unit test?

这是我正在单元测试的类。 目前我正在测试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本身。 然后我会用返回模拟/存根的函数替换 getUniqueConnection 函数。 测试模拟似乎很糟糕,但我看不到任何方法可以实现我所追求的目标。 UniqueConnection是第三方供应商库,无法修改。

你可以做一个UniqueConnectionFactory,并将它的实例传递给FooClass。然后你有

  private function getUniqueConnection( $id ) {
    return $this->uniqueConnectionFactory->create( $id );
  }

通常,这是使用工厂的好处之一 - 您将new运算符排除在类之外,这使您可以更轻松地更改正在创建的对象。

就像 Rambo Coder 说的,这是在课堂上做得太多的问题。我不会想要创建一个工厂,特别是如果你只创建一个特定类的实例。最简单的解决方案是反转创建 UniqueConnection 的责任:

<?php
class FooClass {
  public function doSomething( UniqueConnection $connection ) {
    $connection->doSomethingDestructive( );
  }
}

测试时通过模拟,在真实代码中传递new UniqueConnection( $user->id )

在您可以花时间重构代码以按照 rambo 编码器的建议使用工厂之前,您可以使用部分模拟来返回非破坏性的唯一连接。当你发现自己处于这个位置时,这通常意味着被测班级有不止一项责任。

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();
}
在某些情况下,

以支持不同执行模式的方式创建类非常重要。其中一种情况就是您所要求的。

创建类以支持各种模式。例如

Class Connection {
    private $mode;
    public function setMode($mode) {
         $this -> $mode = $mode;
    }
}

现在,您的doSomethingDestructive可以根据执行模式进行操作。

public function doSomethingDestructive() {
    if($this -> mode === "test") { //if we are in a test mode scenario
        //Log something
        // Or just do some logging and give a message
    } else {
        // do what it was suppose to do
    }
}

下次,当你测试这个类时,你不必担心破坏性函数会意外地破坏一些东西。

  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();分解为一个方法,然后将子类FooClassTestFooClass,并覆盖子类中的新方法。然后,您可以使用子类进行测试,而不会获得不需要的破坏性行为。

例如:

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