在 CakePHP 的模型方法中模拟一个方法

Mock a method within a model method in CakePHP

我是 运行 CakePHP 2.8.X,正在尝试为模型函数编写单元测试。

让我们调用模型 Item,我正在尝试测试它的 getStatus 方法。

但是,该模型在 getStatus 方法中调用了它的 find

所以像这样:

class Item extends Model
{
    public function getStatus($id) {
      // Calls our `$this->Item-find` method
      $item = $this->find('first', [
        'fields' => ['status'],
        'conditions' => ['Item.id' => $id]
      ]);

      $status = $item['status'];

      $new_status = null;

      // Some logic below sets `$new_status` based on `$status`
      // ...

      return $new_status;
    }
}

设置“$new_status”的逻辑有点复杂,这就是为什么我想为它写一些测试。

但是,我不完全确定如何覆盖 Item::getStatus 中的 find 调用。

通常当我想模拟一个模型的函数时,我使用 $this->getMockmethod('find')->will($this->returnValue($val_here)),但我不想完全模拟我的 Item 因为我想测试它的实际 getStatus 功能。

也就是说,在我的测试函数中,我将调用:

// This doesn't work since `$this->Item->getStatus` calls out to
// `$this->Item->find`, which my test suite doesn't know how to compute.
$returned_status = $this->Item->getStatus($id);
$this->assertEquals($expected_status, $returned_status);

那么我如何在我的测试中与我的真实 Item 模型通信,它应该覆盖其对其 find 方法的内部调用?

模拟查找的一种方法是使用特定于测试的子类。

您可以创建一个扩展 item 并覆盖 find 的 TestItem,这样它就不会执行数据库调用。

另一种方法是封装 new_status 逻辑并独立于模型对其进行单元测试

我知道这一定是其他人面临的问题,事实证明 PHPUnit 有一种非常简单的方法来解决这个问题!

This tutorial 基本上给了我答案。

我确实需要创建一个模拟,但是通过只传入 'find' 作为我想要模拟的方法,PHPUnit 有助于将所有其他方法单独留在我的模型中并且 确实不覆盖它们。

上述教程的相关部分是:

Passing an array of method names to your getMock second argument produces a mock object where the methods you have identified

  • Are all stubs,
  • All return null by default,
  • Are easily overridable

Whereas methods you did not identify

  • Are all mocks,
  • Run the actual code contained within the method when called (emphasis mine),
  • Do not allow you to override the return value

意思是,我可以使用那个模拟模型,并直接从它 调用我的 getStatus 方法 。该方法将 运行 它的真实代码,当它到达 find() 时,它将只是 return 我传递给 $this->returnValue.

的任何内容

我使用 dataProvider 将我想要的查找方法传递给 return,以及在我的 assertEquals 调用中测试的结果。

所以我的测试函数看起来像:

/**
 * @dataProvider provideGetItemStatus
 */
public function testGetItemStatus($item, $status_to_test) {
    // Only mock the `find` method, leave all other methods as is
    $item_model = $this->getMock('Item', ['find']);

    // Override our `find` method (should only be called once)
    $item_model
        ->expects($this->once())
        ->method('find')
        ->will($this->returnValue($item));

    // Call `getStatus` from our mocked model.
    // 
    // The key part here is I am only mocking the `find` method,
    // so when I call `$item_model->getStatus` it is actually
    // going to run the real `getStatus` code. The only method
    // that will return an overridden value is `find`.
    // 
    // NOTE: the param for `getStatus` doesn't matter since I only use it in my `find` call, which I'm overriding
    $result = $item_model->getStatus('dummy_id');

    $this->assertEquals($status_to_test, $result);
}

public function provideGetItemStatus() {
    return [
        [
            // $item
            ['Item' => ['id' = 1, 'status' => 1, /* etc. */]],

            // status_to_test
            1
        ],

        // etc...
    ];
}