php 如何对Laravel的Cache::remember功能的所有代码执行路径进行单元测试?

kiayqfof  于 2023-06-04  发布在  PHP
关注(0)|答案(3)|浏览(338)

我是Laravel的Cache::remember功能的忠实粉丝,我在我的服务类上使用它,如下所示:

/**
 * SummaryService
 */
public function getSummaryData(string $userId)
{
    $summaryCacheKey = $userId . '_summary_cache';
    $summaryCacheLifespanMinutes = config('summary_cache_lifespan_minutes');

    return Cache::remember($summaryCacheKey, $summaryCacheLifespanMinutes, function () use ($userId) {

        $summaryResult = [
            'userExists' => false,
            'data' => [],
        ];

        $user = $this->userRepository->findById($userId);

        if ($user) {

            $summaryResult = [
                'userExists' => true,
                'data' => $this->summaryRepository->getSummaryByUserId($user->id),
            ];

        }

        return $summaryResult;

    });
}

这和预期的一样。如果数据存在该高速缓存中,则将其返回,如果不存在,则将其加载并缓存并返回。
现在,我正在尝试对我的SummaryService(两个执行路径)进行单元测试。
通过缓存返回数据的第一部分很容易测试,它看起来像这样:

public function i_can_load_summary_data_via_cache()
{
    // given
    $userId = 'aaaa45-bbbb-cccc-ddddssswwwdw';

    $expectedResult = [
        'userExists' => true,
        'data' => [ ... ],
    ];

    $summaryCacheKey = $userId . '_summary_cache';
    $summaryCacheLifespanMinutes = config('summary_cache_lifespan_minutes');

    Cache::shouldReceive('remember')
        ->once()
        ->with($summaryCacheKey, $summaryCacheLifespanMinutes, Closure::class)
        ->andReturn($expectedResult);

    // when
    $result = $this->summaryService->getSummaryData($userId);

    // then
    $this->assertSame($expectedResult, $result);
}

然而,当我尝试测试数据不存在于缓存中的场景时,我必须加载它(通过模拟存储库),如下所示:

public function i_can_load_summary_data_via_database()
{
    // given
    $userId = 'aaaa45-bbbb-cccc-ddddssswwwdw';

    $expectedResult = [
        'userExists' => true,
        'data' => [ ... ],
    ];

    $user = new User();
    $user->id = $userId;

    $summaryCacheKey = $userId . '_summary_cache';
    $summaryCacheLifespanMinutes = 0;

    Cache::shouldReceive('remember')
        ->once()
        ->with($summaryCacheKey, $summaryCacheLifespanMinutes, \Mockery::on(function() use($user) {
            $this->mockedUserRepository
                ->shouldReceive('findById')
                ->once()
                ->andReturn($user);
            $this->mockedSummaryRepository
                ->shouldReceive('getSummaryByUserId')
                ->once()
                ->with($user->id)
                ->andReturn([ ... ]);
        }))
        ->andReturn($expectedResult);

    // when
    $result = $this->summaryService->getSummaryData($userId);

    // then
    $this->assertSame($expectedResult, $result);
}

测试失败:
未找到Mockery_3_Illuminate_Cache_CacheManager::remember('aaa 45-bbb-cccc-ddddssswwwdw_summary_cache','10',object(Closure))的匹配处理程序。该方法是意外的,或者其参数不匹配此方法的预期参数列表
Object:(array('Closure' => array('class' => 'Closure','properties' => array(),),))
你知道怎么正确测试吗?

hc8w905p

hc8w905p1#

好吧,我好像把事情复杂化了;所以我把它分解了,并像这样稍微修改了一下。
我的服务代码现在看起来像这样:

/**
 * SummaryService
 */

public function getSummaryData(string $userId)
{
    $summaryCacheKey = $userId . '_summary_cache';
    $summaryCacheLifespanMinutes = config('summary_cache_lifespan_minutes');

    return Cache::remember($summaryCacheKey, $summaryCacheLifespanMinutes, function () use ($userId) {
        return $this->loadLiveSummaryData($userId);
    });
}

public function loadLiveSummaryData(string $userId)
{
    $summaryResult = [
        'userExists' => false,
        'data' => [],
    ];

    $user = $this->userRepository->findById($userId);

    if ($user) {

        $summaryResult = [
            'userExists' => true,
            'data' => $this->summaryRepository->getSummaryByUserId($user->id),
        ];

    }

    return $summaryResult;
}

现在,我只需要通过我的单元测试确认:
1.我的服务可以加载缓存版本并匹配调用参数
1.我可以加载实时数据(在那里我可以模拟存储库)
它看起来像这样:

/**
 * @test
 */
public function i_can_load_live_summary_data_for_existing_user()
{
    // given
    $userId = 'aaaa45-bbbb-cccc-ddddssswwwdw';

    $expectedResult = [
        'userExists' => true,
        'data' => [ ... ],
    ];

    $user = new User();
    $user->id = $userId;

    $this->mockedUserRepository
        ->shouldReceive('findById')
        ->once()
        ->andReturn($user);

    $this->mockedSummaryRepository
        ->shouldReceive('getSummaryByUserId')
        ->once()
        ->with($user->id)
        ->andReturn([ ... ]);

    // when
    $result = $this->summaryService->loadLiveSummaryData($userId);

    // then
    $this->assertSame($expectedResult, $result);
}

/**
 * @test
 */
public function i_expect_cache_to_be_called_when_loading_summary_data_for_specific_user()
{
    // given
    $userId = 'aaaa45-bbbb-cccc-ddddssswwwdw';

    $expectedResult = [
        'userExists' => true,
        'data' => [ ... ],
    ];

    $summaryCacheKey = $userId . '_summary_cache';
    $summaryCacheLifespanMinutes = 10;

    Cache::shouldReceive('remember')
        ->once()
        ->with($summaryCacheKey, $summaryCacheLifespanMinutes, \Mockery::on(function($value) {
            return is_callable($value);
        }))
        ->andReturn($expectedResult);

    // when
    $result = $this->summaryService->getSummaryData($userId);

    // then
    $this->assertSame($expectedResult, $result);
}

让我知道是否有更好或“正确”的方法来做到这一点。

ppcbkaq5

ppcbkaq52#

有一个类似的情况,我想测试两个路径,当数据通过该高速缓存返回时,当回调函数被执行时。
对我来说,关键是不要使用任何facade mock方法(例如:Cache::shouldReceive('remember')),然后回调代码将运行。
现在看起来很明显:(

xjreopfe

xjreopfe3#

我认为所提出的解决方案可以进一步改进,因为实际上要使其工作,您需要将代码移动到另一个方法中并使其公开。所以最后,你在类的公共契约中创建了一个新的方法,只是为了使它可测试。类的用户可以绕过该高速缓存机制,这在某些情况下可能会导致有害的后果。我们可以将此方法更改为protected或private,但这样我们就必须在测试中使用Reflection来更改方法的可见性以对其进行测试。
这里有一种方法可以做到这一点,而不改变测试的主题(我们正在测试的代码)。不需要提取新方法或使用反射。

$cacheMock = \Mockery::mock(\Illuminate\Cache\Repository::class, function (MockInterface $mock) {
    $mock->shouldReceive('remember')
        >once()
        >andReturnUsing(fn (string $key, int $ttl, \Closure $callback) => $callback());
});

andReturnUsing是一个使用回调的方法,您可以使用该回调来生成该方法应该返回的内容。回调函数接收传递给remember方法的原始参数。在本例中,我获取remember方法的第三个参数,在本例中,该参数是在没有缓存命中时执行的回调。然后我执行该高速缓存回调并返回它返回的任何内容。
注意:我通常更喜欢将\Illuminate\Cache\Repository接口传递给我的服务类的构造函数,而不是直接在服务类中使用\Cache facade。我还没有测试过它,但我非常肯定你可以直接在facade上使用mocking方法,而不是像我一样使用Repository类。

相关问题