使用PHPUnit模拟未在测试类中指定的程序确定的方法


Using PHPUnit to mock a programatically determined method not specified in the class under test

使用PHPUnit 3.6我试图在下面的控制器类中测试exec()方法。这种方法做两件事:

  1. 根据对象的现有属性确定要调用的方法的名称,以及
  2. 如果确定的控制器方法是可调用的,则执行该方法,如果不是,则该方法抛出异常

(简化的)源代码如下:

abstract class CLIController extends Controller
{
  /* irrelevant class details here */
  public function exec()
  {
    $action = ! empty($this->opts->args[0])
      ? $this->opts->args[0]
      : $this->default_action;
    if ( ! $action || ! is_callable(array($this, $action))) {
      $msg = 'Invalid controller action specified';
      throw new LogicException($msg);
    } else {
      $this->$action(); // <---- trying to get code coverage on this line!
    }
  }
}

所以我的问题是

我不知道如何覆盖这部分代码:

} else {
  $this->$action();
}

因为我不知道如何(或者是否可能)测试在抽象类的上下文中名称未知的方法的调用再次:要调用的方法是在子类中声明的通常我只会模拟一个抽象方法,但在这种情况下我不能,因为该方法还不存在——它将由一个子类指定。

答案可能是什么

  • ???这一行可能甚至不需要覆盖,因为它本质上依赖于PHP正确调用可调用类方法的能力。如果我成功地测试了exec()在应该抛出异常的时候抛出异常,我就知道问题行的正确运行取决于PHP的正确运行。这是否会使最初测试它的必要性失效
  • 如果有某种方法模拟抽象类,并创建一个已知名称的方法添加到模拟类中,这将解决我的问题,也是我迄今为止一直尝试做但没有成功的事情
  • 我知道我可以创建一个具有已知方法名的子类,但我认为创建一个具体的子类来测试抽象的父类不是一个好主意
  • 可能是我需要重构。我不想做的一件事是让子类自己实现exec()函数

我所尝试的

  • 使用PHP的一些反射功能是徒劳的——这可能是因为我自己对反射缺乏经验,而不是它无法处理这种情况
  • 回顾PHPUnit手册和API文档。不幸的是,尽管PHPUnit非常棒,但我经常发现API文档有点轻
<小时>

我真的很感谢任何关于如何最好地在这里进行的指导。提前谢谢。

我不同意你的规定,即"创建一个具体的子类来测试一个抽象的父类不是一个好主意。"我在测试抽象类时经常这样做,通常在测试后命名具体的子类别以明确这一点。

class CLIControllerTest extends PHPUnit_Framework_TestCase
{
    public function testCallsActionMethod()
    {
        $controller = new CLIControllerTest_WithActionMethod(...);
        // set $controller->opts->args[0] to 'action'
        $controller->exec();
        self::assertTrue($controller->called, 'Action method was called');
    }
}
class CLIControllerTest_WithActionMethod extends CLIController
{
    public $called = false;
    public function action() {
        $this->called = true;
    }
}

进行此测试的代码很简单,可以很容易地通过检查进行验证。

我很好奇,为什么用is_callable而不是method_exists来避免创建数组?这可能只是个人偏好,但我想知道是否存在语义差异。