用于模拟由新类对象调用的方法的单元测试

时间:2011-10-13 21:28:49

标签: php mocking phpunit

我正在编写现有代码的单元测试,就像这样

class someClass {
    public function __construct() { ... }

    public function someFoo($var) {
        ...
        $var = "something";
        ...
        $model = new someClass();
        model->someOtherFoo($var);
    }

    public someOtherFoo($var){
         // some code which has to be mocked
    }
}

在这里,我应该如何模拟对“someOtherFoo”函数的调用,使其在some code内不执行“someOtherFoo”?

class someClassTest {
   public function someFoo() {
      $fixture = $this->getMock('someClass ', array('someOtherFoo'));
      $var = "something";
      ....
      // How to mock the call to someOtherFoo() here
   }

}

是否可以模拟构造函数,以便返回我自己构造的函数或变量?

由于

3 个答案:

答案 0 :(得分:28)

无论您在被测方法中有new XXX(...),您都注定要失败。将实例化提取到同一类的新方法 - createSomeClass(...)。这允许您创建一个被测试类的部分模拟,它从新方法返回一个stubbed或mock值。

class someClass {
    public function someFoo($var) {
        $model = $this->createSomeClass();  // call method instead of using new
        model->someOtherFoo($var);
    }

    public function createSomeClass() {  // now you can mock this method in the test
        return new someClass();
    }

    public function someOtherFoo($var){
         // some code which has to be mocked
    }
}

在测试中,在您调用createSomeClass()的实例中模拟someFoo(),并在您从第一次模拟调用返回的实例中模拟someOtherFoo()

function testSomeFoo() {
    // mock someOtherFoo() to ensure it gets the correct value for $arg
    $created = $this->getMock('someClass', array('someOtherFoo'));
    $created->expects($this->once())
            ->method('someOtherFoo')
            ->with('foo');

    // mock createSomeClass() to return the mock above
    $creator = $this->getMock('someClass', array('createSomeClass'));
    $creator->expects($this->once())
            ->method('createSomeClass')
            ->will($this->returnValue($created));

    // call someFoo() with the correct $arg
    $creator->someFoo('foo');
}

请记住,因为实例正在创建同一个类的另一个实例,所以通常会涉及两个实例。如果它更清晰,你可以在这里使用相同的模拟实例。

function testSomeFoo() {
    $fixture = $this->getMock('someClass', array('createSomeClass', 'someOtherFoo'));

    // mock createSomeClass() to return the mock
    $fixture->expects($this->once())
            ->method('createSomeClass')
            ->will($this->returnValue($fixture));

    // mock someOtherFoo() to ensure it gets the correct value for $arg
    $fixture->expects($this->once())
            ->method('someOtherFoo')
            ->with('foo');

    // call someFoo() with the correct $arg
    $fixture->someFoo('foo');
}

答案 1 :(得分:6)

您可以在模拟类名称前加上overload:

查看Mocking Hard Dependencies上的文档。

您的示例如下:

/**
 * @runTestsInSeparateProcesses
 * @preserveGlobalState disabled
 */
class SomeClassTest extends \PHPUnit\Framework\TestCase
{
    public function test_some_foo()
    {
        $someOtherClassMock = \Mockery::mock('overload:SomeOtherClass');
        $someOtherClassMock->shouldReceive('someOtherFoo')
            ->once()
            ->with('something')
            ->andReturn();

        $systemUnderTest = new SomeClass();

        $systemUnderTest->someFoo('something');
    }

}

我添加了@runTestsInSeparateProcesses注释,因为通常模拟类也将在其他测试中使用。如果没有注释,则自动加载器将由于class already exists错误而崩溃。

如果这是在测试套件中唯一使用模拟类的地方,则应删除注释。

答案 2 :(得分:2)

我在这里试图通过白盒测试类__constructor来确保它自己调用一个类方法,并将一些数据传入__constructor。

如果其他人出于同样的原因在这里,我想我会分享我最终使用的方法(没有在这个问题中使用的工厂风格的createSomeClass()方法)。

<?php
class someClass {

  public function __constructor($param1) {
    // here is the method in the constructor we want to call
    $this->someOtherFoo($param1);
  }

  public function someOtherFoo($var){  }

}

现在PHPUnit测试:

<?php
$paramData = 'someData';

// set up the mock class here
$model = $this->getMock('someClass', 
  array('someOtherFoo'), // override the method we want to check
  array($paramData) // we need to pass in a parameter to the __constructor
);

// test that someOtherFoo() is called once, with out test data
$model->expects($this->once())
      ->with($paramData)
      ->method('someOtherFoo');

// directly call the constructor, instead of doing "new someClass" like normal
$model->__construct($paramData);