【问题标题】:Unit test for mocking a method called by new class object用于模拟由新类对象调用的方法的单元测试
【发布时间】:2011-12-07 07:40:46
【问题描述】:

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

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”的调用,使其不会在someOtherFoo 内执行“some code”?

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

}

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

谢谢

【问题讨论】:

  • 你想要达到什么目的?为什么要模拟 someFoo 而不是对其进行测试?
  • 我正在尝试测试函数test()。为了使其可单元测试,我想模拟对someFoo() 的调用,以便我可以测试函数test 而不依赖于someFoo。这对你有意义还是我应该再举一个例子?谢谢
  • 我想我会测试one::someFoo(),然后测试one::test();如果您已经测试过one::someFoo(),那么one::test() 依赖它并不重要,不是吗?毕竟,这就是在实际使用中会发生的事情。我理解您为什么要模拟数据库连接或 HTTP 客户端,但我并没有真正看到这里的问题。您可以添加一个静态属性$mocking 和一个one::setMocking(true),然后在one::someFoo() 方法中添加if (self::$mocking) return;,在您的测试用例中打开或关闭标志。
  • 你是对的。我可以在这里做。但问题是这是遗留代码,所以我不知道我需要更改多少地方。
  • Sumtik,第一手测试遗留代码从来都不是一件简单的事情。这主要是因为遗留代码没有测试。你正在改变这个,所以这是一些工作。从一个类开始,看看它是如何工作的。

标签: php mocking phpunit


【解决方案1】:

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

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

在测试中,在您调用someFoo() 的实例中模拟createSomeClass(),并在您从第一个模拟调用返回的实例中模拟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');
}

【讨论】:

  • 嗨,David,我已经更改了我的代码以使其更具可读性。很抱歉我匆忙写了上一篇。所以现在我正在做@Daren 建议的事情,我的测试是正确的,但我怀疑我将来也会面临这个问题,所以我应该在这里问,因为我无法从框架中获得任何帮助。谢谢!
  • 我会遵循我的第一个选项,将构造函数调用提取到一个新方法。我已经更新了我的答案以展示它是如何工作的。
  • 感谢您的解释,大卫!我也在做同样的事情,但很困惑。我创建了自己的框架,该框架将类名和函数名作为输入。然后我使用reflection 获取该类中的所有函数并从列表中删除被测函数。然后我创建了一个包含所有其他函数的模拟,所以现在我有一个模拟 obj 并模拟了所有其他函数。这样我就可以减少测试代码中的冗余行。但是想知道是否有任何方法可以在不更改现有代码的情况下做到这一点。好吧,感谢您消除了我的疑问!
  • 这种方法存在问题。实际上 createSomeClass() 不应该是公开的,因为它与类的接口无关,它暴露了实现的内部细节。换句话说,传递 new someClass() 将是一个更好的解决方案,但对于某些类别,它看起来也很糟糕(例如,对于 MVC 中的控制器,在模型诞生时应该是底层)。
【解决方案2】:

你可以在你的模拟类名前加上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 错误而崩溃。

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

【讨论】:

  • 有什么办法可以用 PHP 单元本身而不是使用 Mockery 来创建模拟?
【解决方案3】:

我在这里找到了尝试对类 __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);

【讨论】:

    猜你喜欢
    • 2014-06-07
    • 1970-01-01
    • 2017-12-29
    • 1970-01-01
    • 2020-05-17
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多