You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何用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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.17 00:58:14