如何用Mockery模拟包含属性与方法的链式调用?
解决Mockery混合属性与方法链式调用的测试问题
问题核心
要测试Newsletter::resubscribe方法发送给Klaviyo API的请求体正确性,同时避免发起真实API请求。但目标调用链$client->request->Profiles->subscribeProfiles($body)里,request和Profiles是公共属性,subscribeProfiles是方法——Mockery的Demeter链式语法只支持纯方法调用,直接用链式写法会抛出Undefined property: Mockery\CompositeExpectation::$request错误,现有测试要么触发真实请求,要么没法验证请求体内容。
解决方案:分层Mock调用链
Mockery不直接支持属性+方法的混合链式模拟,需要从内到外逐层Mock每个属性返回的对象,构建完整的模拟调用链:
完整可运行测试代码
use ApiClient; use Newsletter; use Mockery; use KlaviyoAPI\KlaviyoAPI; public function testSubscribeUser(): void { $apiKey = 'api_key_12345'; $listId = 'list_id_12345'; $email = 'test@test.com'; // 定义预期的请求体 $expectedBody = [ 'data' => [ 'type' => 'profile-subscription-bulk-create-job', 'attributes' => [ 'list_id' => $listId, 'custom_source' => 'Resubscribe user', 'subscriptions' => [ [ 'channels' => [ 'email' => [ 'MARKETING', ], ], 'email' => $email, ], ], ], ], ]; // 1. Mock最内层的Profiles对象,指定subscribeProfiles方法的行为 $profilesMock = Mockery::mock(); $profilesMock->shouldReceive('subscribeProfiles') ->with(Mockery::on(function ($receivedBody) use ($expectedBody) { // 精确比对请求体内容,也可根据需求做灵活校验 return $receivedBody === $expectedBody; })) ->once() ->andReturnNull(); // 2. Mock KlaviyoAPI对象,让它的Profiles属性返回上面的Mock实例 $klaviyoApiMock = Mockery::mock(KlaviyoAPI::class); $klaviyoApiMock->Profiles = $profilesMock; // 3. Mock ApiClient对象,让它的request属性返回KlaviyoAPI的Mock实例 $clientMock = Mockery::mock(ApiClient::class, [$apiKey]); $clientMock->request = $klaviyoApiMock; // 执行测试方法 $response = resolve(Newsletter::class)->resubscribe($clientMock, $listId, $email); // 断言返回值符合预期 static::assertNull($response); // 验证所有Mock的调用期望都被满足 Mockery::close(); }
关键说明
- 分层Mock逻辑:从最内层的方法开始,逐层向外模拟每个属性的返回对象,确保整个调用链完全被模拟,不会触发真实API请求。
- 灵活校验请求体:用
Mockery::on()闭包可以做更复杂的校验,比如忽略某些动态生成的字段,或者做深度数组比对。 - 完全隔离外部依赖:所有涉及Klaviyo API的对象都用Mock替代,彻底避免网络请求。
可选优化方案:封装调用链简化测试
如果觉得分层Mock太繁琐,可以给ApiClient添加封装方法,隐藏内部的属性链式调用:
// 在ApiClient类中新增方法 public function subscribeProfiles(array $body) { return $this->request->Profiles->subscribeProfiles($body); }
然后修改Newsletter::resubscribe的调用逻辑:
// Newsletter类的resubscribe方法 return $client->subscribeProfiles($body);
这样测试代码会大幅简化,只需要MockApiClient的封装方法:
public function testSubscribeUser(): void { $apiKey = 'api_key_12345'; $listId = 'list_id_12345'; $email = 'test@test.com'; $expectedBody = [/* 同之前的预期请求体 */]; $clientMock = Mockery::mock(ApiClient::class, [$apiKey]); $clientMock->shouldReceive('subscribeProfiles') ->with($expectedBody) ->once() ->andReturnNull(); $response = resolve(Newsletter::class)->resubscribe($clientMock, $listId, $email); static::assertNull($response); Mockery::close(); }
这种方式不仅简化了测试,还符合迪米特法则,让代码的耦合度更低、更易维护。
内容的提问来源于stack exchange,提问作者Cudos
相关产品推荐
相关产品推荐

