Laravel中如何模拟带参数的容器解析类?
为什么你的模拟不生效?
当你调用app(MyClass::class, [$data])时,Laravel容器会直接执行你定义的绑定闭包——而这个闭包里是硬编码调用MyClass::from($parameters[0])创建原始类实例,完全绕过了你之前创建的模拟(spy/mock)对象。只有当你不带参数调用app(MyClass::class)时,容器才会返回你注册的模拟实例,这就是问题的根源。
快速修复:测试时覆盖容器绑定
要让带参数调用也返回模拟实例,你需要在测试中覆盖原来的容器绑定,让它返回你的spy/mock,而不是创建新的原始类。示例代码如下:
// 创建你的spy $spy = spy(MyClass::class); // 覆盖容器绑定,让带参数调用返回spy $this->app->bind(MyClass::class, function ($app, array $parameters) use ($spy) { // 如果你的逻辑需要根据传入的$data做处理,可以在这里给spy设置相应行为 // 比如:$spy->shouldReceive('someMethod')->with($parameters[0])->... return $spy; }); // 现在调用带参数的app()会返回spy app(MyClass::class, [$data]); $spy->shouldHaveReceived('reactPHPIdle');
如果你的业务逻辑依赖传入的$data来初始化MyClass的状态,你可以在spy上预先设置对应方法的期望行为,确保测试覆盖到相关逻辑。
更优的重构方案:提升可测试性和可维护性
上面的修复是临时解决方案,从长期来看,重构你的类和容器绑定方式能从根本上解决这类问题:
方案1:将构造函数改为public
把MyClass的构造函数改为public,让静态方法from作为便捷创建方式而非唯一入口。这样容器可以更灵活地解析实例,测试时也更容易模拟:
class MyClass { // 改为public构造函数 public function __construct(AnotherClass $data) { // do stuff } // 保留静态便捷方法 public static function from(AnotherClass $data): self { return new self($data); } }
对应的容器绑定可以简化为:
$this->app->bind(MyClass::class, function ($app, array $parameters) { return new MyClass($parameters[0]); });
测试时,你可以直接模拟MyClass,容器在解析时会自动返回模拟实例,无需额外处理参数逻辑。
方案2:引入工厂类解耦对象创建
将MyClass的创建逻辑抽离到独立的工厂类中,通过接口依赖注入,这样测试时只需模拟工厂接口即可:
// 定义工厂接口 interface MyClassFactoryInterface { public function create(AnotherClass $data): MyClass; } // 实现具体工厂 class MyClassFactory implements MyClassFactoryInterface { public function create(AnotherClass $data): MyClass { return MyClass::from($data); } }
在Laravel容器中绑定接口和实现:
$this->app->bind(MyClassFactoryInterface::class, MyClassFactory::class);
然后在需要使用MyClass的地方,注入MyClassFactoryInterface并调用create方法:
// 示例:在控制器中使用 public function someMethod(MyClassFactoryInterface $factory) { $data = new AnotherClass(); $myClassInstance = $factory->create($data); // ... 业务逻辑 }
测试时,你只需模拟工厂接口,让它返回MyClass的spy/mock:
$myClassSpy = spy(MyClass::class); $factoryMock = mock(MyClassFactoryInterface::class); $factoryMock->shouldReceive('create')->with(InstanceOf(AnotherClass::class))->andReturn($myClassSpy); $this->app->instance(MyClassFactoryInterface::class, $factoryMock); // 执行测试逻辑后验证 $myClassSpy->shouldHaveReceived('reactPHPIdle');
这种方式彻底解耦了对象创建和业务逻辑,让测试变得更灵活,也符合依赖倒置原则。
内容的提问来源于stack exchange,提问作者Cata

