Mockery项目教程:如何在类内部模拟其他类的实例
理解问题场景
在单元测试中,我们经常需要模拟(mock)某些类的行为,特别是当这些类执行一些我们不希望在测试中实际执行的操作时(如数据库访问、网络请求等)。Mockery作为PHP的一个强大的模拟框架,可以帮助我们实现这一目标。
但在某些特定场景下,直接使用Mockery会遇到挑战。比如当一个类在其方法内部直接实例化另一个类时,传统的模拟方法就会失效。让我们通过一个具体例子来理解这个问题。
示例代码分析
假设我们有两个类:
// Point.php
namespace App;
class Point {
public function setPoint($x, $y) {
echo "Point (" . $x . ", " . $y . ")" . PHP_EOL;
}
}
// Rectangle.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
对象。如果我们想测试create
方法,但不希望实际执行Point
的setPoint
方法或Rectangle
的draw
方法,传统的模拟方法会遇到问题。
传统模拟方法的问题
尝试直接模拟这两个类:
class MyTest extends PHPUnit\Framework\TestCase {
public function testCreate() {
$point = Mockery::mock("App\Point");
$point->shouldReceive("setPoint")->andThrow(Exception::class);
$rect = Mockery::mock("App\Rectangle")->makePartial();
$rect->shouldReceive("draw");
$rect->create(0, 0, 100, 100); // 不会抛出异常
Mockery::close();
}
}
这个测试不会按预期工作,因为Point
类已经被自动加载,Mockery无法拦截new Point()
的调用。
问题根源
问题的核心在于PHP的类加载机制。当类已经被加载后,Mockery无法拦截对该类的直接实例化。在复杂的依赖关系中,这会导致一系列连锁反应:
A // 主加载入口
|- B // 另一个加载入口
| |-E
| +-G
|
|- C // 另一个加载入口
| +-F
|
+- D
每个加载入口都会触发一系列类的加载,使得在这些路径上的类难以被模拟。
解决方案:工厂方法模式
解决这个问题的有效方法是引入工厂方法,将对象的创建封装到一个单独的方法中:
class Rectangle {
public function newPoint() {
return new Point();
}
public function create($x1, $y1, $x2, $y2) {
$a = $this->newPoint();
$a->setPoint($x1, $y1);
// ... 其他点
}
// ...
}
然后在测试中,我们可以模拟这个工厂方法:
class MyTest extends PHPUnit\ramework\TestCase {
public function testCreate() {
$point = Mockery::mock("App\Point");
$point->shouldReceive("setPoint")->andThrow(Exception::class);
$rect = Mockery::mock("App\Rectangle")->makePartial();
$rect->shouldReceive("draw");
// 让newPoint方法返回我们的模拟Point对象
$rect->shouldReceive("newPoint")->andReturn($point);
$this->expectException(Exception::class);
$rect->create(0, 0, 100, 100);
Mockery::close();
}
}
为什么这个方案有效
- 控制反转:通过将对象创建的责任交给专门的方法,我们可以在测试中控制这个方法的行为
- 避免全局状态污染:不需要修改全局的类加载机制
- 可测试性:每个类的创建点都可以被单独模拟
- 代码结构更清晰:对象创建逻辑集中在一处,便于维护
更复杂的场景处理
对于更复杂的依赖关系,可以按照以下步骤处理:
- 识别程序流中的下一个加载入口
- 为这些入口点创建工厂方法
- 在测试中,逐步用模拟对象替换这些工厂方法的返回值
- 为每个模拟对象预设预期的行为
最佳实践建议
- 避免在业务逻辑中直接使用
new
关键字:尽量通过依赖注入或工厂方法获取对象实例 - 保持工厂方法简单:它们应该只负责创建对象,不包含业务逻辑
- 合理设计类结构:考虑使用依赖注入容器来管理对象创建
- 适度使用:不是所有类都需要这种处理,只对那些需要在测试中模拟的类使用
总结
通过使用工厂方法模式,我们可以在Mockery框架中有效地解决类内部实例化其他类的模拟问题。这种方法既保持了代码的清晰性,又提供了良好的可测试性。记住,良好的代码设计往往能带来更好的测试体验,而Mockery这样的工具则能帮助我们验证这些设计是否正确实现。
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考