在单元测试中模拟被测对象是不是很糟糕?

时间:2012-08-13 22:59:37

标签: php unit-testing phpunit

这是我正在进行单元测试的课程。目前我正在测试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而不是使用真实的。{/ p>

我没有看到任何方法来注射我的嘲笑UniqueConnection。我会为UniqueConnection创建FooClass构造函数参数,但正如您所看到的,根据doSomething函数的参数创建一个新参数,并且可能是所有唯一ID被叫的人不知道提前知道。

我能看到的唯一选择是测试FooClass而不是FooClass本身的模拟。然后我将getUniqueConnection函数替换为返回模拟/存根的函数。这对于模拟测试来说似乎很糟糕,但我没有看到任何方法来实现我的目标。 UniqueConnection是第三方供应商库,无法修改。

5 个答案:

答案 0 :(得分:4)

您可以创建一个UniqueConnectionFactory,并将其实例传递给FooClass。那你有

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

一般来说,这是使用工厂的好处之一 - 您将new运算符保留在类之外,这样您就可以更轻松地更改正在创建的对象。

答案 1 :(得分:2)

在您可以花时间重构代码以使用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();
}

答案 2 :(得分:2)

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

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

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

答案 3 :(得分:0)

在某些情况下,以支持不同执行模式的方式创建类非常重要。其中一个案例就是你要求的。

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

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
  }

答案 4 :(得分:0)

在这种情况下,您想要的不是模拟对象,而是测试子类。将$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 );
  }
}