假设我想测试一个简单的助手,它以类名作为参数并进行重定向。
如果从几个控制器内部的许多地方调用该函数,我应该如何测试它?我是否应该测试在整个代码中作为参数传递的每个类名(我自己将它们写在提供程序函数中)?或者是否有一个神奇的功能可以为我做到这一点?
您的问题是依赖注入——如果正确完成(而不是大多数流行的框架如何“实现”它)——被吹捧为代码可测试性的终极目标的确切原因。
要了解原因,让我们看看“辅助函数”和面向类的编程如何使您的控制器难以测试。
class Helpers {
public static function myHelper() {
return 42;
}
}
class MyController {
public function doSomething() {
return Helpers::myHelper() + 100;
}
}
单元测试的重点是验证代码的“单元”是否独立工作。如果您不能隔离功能,那么您的测试将毫无意义,因为它们的结果可能会受到所涉及的其他代码行为的污染。这可能导致统计学家所说的 I 型和 II 型错误:基本上,这意味着您可以获得可能对您撒谎的测试结果。
在上面的代码中,不能轻易地模拟 helper 以确定它MyController::doSomething
在完全隔离外部影响的情况下工作。为什么不?因为我们不能“模拟”辅助方法的行为来保证我们的doSomething
方法实际上将 100 添加到辅助结果中。我们坚持使用助手的确切行为(返回 42)。这是正确的面向对象和控制反转完全消除的问题。让我们考虑一个例子:
如果MyController
要求它的依赖项而不是使用静态辅助函数,则模拟外部影响变得微不足道。考虑:
interface AnswerMachine {
public function getAnswer();
}
class UltimateAnswerer implements AnswerMachine {
public function getAnswer() {
return 42;
}
}
class MyController {
private $answerer;
public function __construct(AnswerMachine $answerer) {
$this->answerer = $answerer;
}
public function doSomething() {
return $this->answerer->getAnswer() + 100;
}
}
现在,测试起来非常简单,实际上它MyController::doSomething
确实将 100 添加到从应答机返回的任何内容中:
// test file
class StubAnswerer implements AnswerMachine {
public function getAnswer() {
return 50;
}
}
$stubAnswer = new StubAnswerer();
$testController = new MyController($stubAnswerer);
assert($testController->doSomething() === 150);
此示例还演示了如何在代码中正确使用接口可以大大简化测试过程。像 PHPUnit 这样的测试框架可以很容易地模拟接口定义以准确地执行您希望它们执行的操作,以便测试代码单元的隔离功能。
因此,我希望这些非常简单的示例能够展示依赖注入在测试代码时的强大功能。但更重要的是,我希望他们能证明,如果您选择的框架使用静态(只是全局的另一个名称)、单例和辅助函数,为什么您应该小心。
您无法针对需要测试的所有功能测试每种可能的参数组合;它会花费你比宇宙生命更长的时间。所以你使用人类智能(有些人可能称之为作弊;-)。只测试一次,在本例中使用模拟控制器作为参数。
然后看看你的代码,问问自己是否有任何其他传入的对象真的会让它的行为有所不同。对于您描述为“简单帮手”的东西,答案可能是否定的。但是,如果是,如何?创建另一个模拟不同行为的模拟控制器类。例如,这第二个控制器可能没有您的助手类期望调用的功能。您希望抛出异常。为此创建单元测试。
重复直到满意。