Mockery项目教程:如何在类内部模拟其他类的实例

Mockery项目教程:如何在类内部模拟其他类的实例

mockery Mockery是一个针对PHP的轻量级模拟对象框架,专为进行依赖注入和隔离测试设计,通过创建模拟对象替代真实依赖,使得单元测试更便捷、高效。 mockery 项目地址: https://gitcode.com/gh_mirrors/mo/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方法,但不希望实际执行PointsetPoint方法或Rectangledraw方法,传统的模拟方法会遇到问题。

传统模拟方法的问题

尝试直接模拟这两个类:

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();
    }
}

为什么这个方案有效

  1. 控制反转:通过将对象创建的责任交给专门的方法,我们可以在测试中控制这个方法的行为
  2. 避免全局状态污染:不需要修改全局的类加载机制
  3. 可测试性:每个类的创建点都可以被单独模拟
  4. 代码结构更清晰:对象创建逻辑集中在一处,便于维护

更复杂的场景处理

对于更复杂的依赖关系,可以按照以下步骤处理:

  1. 识别程序流中的下一个加载入口
  2. 为这些入口点创建工厂方法
  3. 在测试中,逐步用模拟对象替换这些工厂方法的返回值
  4. 为每个模拟对象预设预期的行为

最佳实践建议

  1. 避免在业务逻辑中直接使用new关键字:尽量通过依赖注入或工厂方法获取对象实例
  2. 保持工厂方法简单:它们应该只负责创建对象,不包含业务逻辑
  3. 合理设计类结构:考虑使用依赖注入容器来管理对象创建
  4. 适度使用:不是所有类都需要这种处理,只对那些需要在测试中模拟的类使用

总结

通过使用工厂方法模式,我们可以在Mockery框架中有效地解决类内部实例化其他类的模拟问题。这种方法既保持了代码的清晰性,又提供了良好的可测试性。记住,良好的代码设计往往能带来更好的测试体验,而Mockery这样的工具则能帮助我们验证这些设计是否正确实现。

mockery Mockery是一个针对PHP的轻量级模拟对象框架,专为进行依赖注入和隔离测试设计,通过创建模拟对象替代真实依赖,使得单元测试更便捷、高效。 mockery 项目地址: https://gitcode.com/gh_mirrors/mo/mockery

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

汪萌娅Gloria

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值