在类中模拟类
想象一种情况,你需要在同一个方法中创建一个类的实例并使用它:
想象一种情况,你需要在同一个方法中创建一个类的实例并使用它:
// Point.php
<?php
namespace App;
class Point {
public function setPoint($x, $y) {
echo "Point (" . $x . ", " . $y . ")" . PHP_EOL;
}
}
// Rectangle.php
<?php
namespace App;
use App\Point;
class Rectangle {
public function create($x1, $y1, $x2, $y2) {
$a = new Point();
$a->setPoint($x1, $y1);
$b = new Point();
$b->setPoint($x2, $y1);
$c = new Point();
$c->setPoint($x2, $y2);
$d = new Point();
$d->setPoint($x1, $y2);
$this->draw([$a, $b, $c, $d]);
}
public function draw($points) {
echo "Do something with the points";
}
}
并且你想测试 Rectangle->create() 中的逻辑是否正确调用了每个需要的东西 —— 在这种情况下调用了 Point->setPoint(),但 Rectangle->draw() 会做一些图形相关的事,而你不想让它真正执行。
你为 App\Point 和 App\Rectangle 设置了 mocks:
<?php
class MyTest extends PHPUnit\Framework\TestCase {
public function testCreate() {
$point = Mockery::mock("App\Point");
// 检查我们的 mock 是否被调用
$point->shouldReceive("setPoint")->andThrow(Exception::class);
$rect = Mockery::mock("App\Rectangle")->makePartial();
$rect->shouldReceive("draw");
$rect->create(0, 0, 100, 100); // 不会抛出异常
Mockery::close();
}
}
但是测试并没有生效。原因是:mocking 依赖于类尚未被加载,而类一旦被自动加载,单独为 App\Point 设置的 mock 就没用了,你可以看到 echo 被真正执行了。
不过对于加载顺序中的第一个类(例如 App\Rectangle),mock 是有效的,因为它会去加载 App\Point 类。在更复杂的例子中,会有一个单独的入口类触发整个加载(use Class),例如:
A // 主加载发起者
|- B // 另一个加载发起者
| |-E
| +-G
|
|- C // 另一个加载发起者
| +-F
|
+- D
这基本上意味着加载阻止了 mocking,而对于每个这样的加载发起者,都需要实现一个变通方案。重载是一种方法,但它会污染全局状态。在这里我们尝试完全避免全局状态污染,通过在特定加载发起者处自定义 new Class() 行为,这样在几个关键点就能很容易地 mock。
话虽如此,虽然我们不能阻止类加载,但我们可以返回 mocks。来看 Rectangle->create() 方法:
class Rectangle {
public function newPoint() {
return new Point();
}
public function create($x1, $y1, $x2, $y2) {
$a = $this->newPoint();
$a->setPoint($x1, $y1);
...
}
...
}
我们创建一个自定义函数来封装 new 关键字,否则它会直接使用自动加载的 App\Point 类,在测试中我们 mock 这个函数,使其返回我们的 mock:
<?php
class MyTest extends PHPUnit\Framework\TestCase {
public function testCreate() {
$point = Mockery::mock("App\Point");
// 检查我们的 mock 是否被调用
$point->shouldReceive("setPoint")->andThrow(Exception::class);
$rect = Mockery::mock("App\Rectangle")->makePartial();
$rect->shouldReceive("draw");
// 将 App\Point mock 传入 App\Rectangle
// 作为替代直接在代码中使用 new App\Point()
$rect->shouldReceive("newPoint")->andReturn($point);
$this->expectException(Exception::class);
$rect->create(0, 0, 100, 100);
Mockery::close();
}
}
如果现在运行这个测试,它应该会通过。对于更复杂的情况,我们会在程序流中找到下一个加载点,并继续通过包装和传递带有预定义行为的 mock 实例到已存在的类中。