Laravel - 如何使用 mockery 对更新用户数据的中间件进行单元测试

Posted

技术标签:

【中文标题】Laravel - 如何使用 mockery 对更新用户数据的中间件进行单元测试【英文标题】:Laravel - How to unit test a middleware updating user's data using mockery 【发布时间】:2021-03-26 02:28:32 【问题描述】:

我有以下中间件来更新用户的 last_seen_at 字段,我只将它用于经过身份验证的用户可以调用它的路由:

// app/Http/Middleware/LastSeen.php

public function handle($request, Closure $next)

    $user = $request->user();

    $user->last_seen_at = now();

    $user->save();

    return $next($request);

// app/Http/Kernel.php

protected $routeMiddleware = [
    // other middleware
    'lastseen' => \App\Http\Middleware\LastSeen::class
]
// routes/api.php

Route::group([
    'middleware' => ['jwt', 'lastseen']
], function () 
    // routes
);

我为测试中间件编写了以下单元测试:

// tests/Unit/LastSeenMiddlewareTest.php

/** @test */
public function doesLastSeenMiddlewareUpdateUsersLastSeenAt()

    $user = Mockery::mock(User::class);

    $user->shouldReceive('setAttribute')->passthru();
    $user->shouldReceive('save')->once();

    $user = $user->makePartial();

    app()->instance(User::class, $user);

    $request = Mockery::mock(Request::class)
        ->shouldReceive('user')
        ->once()
        ->andReturn()
        ->getMock();

    $middleware = new LastSeen;

    $middleware->handle($request, function () );

通过运行测试,我得到一个错误:ErrorException: Creating default object from empty value。但是,我不明白这个问题能够解决它。

【问题讨论】:

我将使用功能测试对其进行测试。基本上,在您的测试中创建一个分配了中间件的路由,访问该路由,在登录的用户上声明更新的last_seen 检查这篇文章laravel-news.com/testing-laravel-middleware @ml59 所以你说不可能进行单元测试?以前的开发人员为他们实现的大多数中间件编写了单元测试。我只是想保持一致。 我从不这么说。 Laravel 社区中有很多关于如何测试MiddlewareRequest 等的讨论。有些使用单元测试,有些使用功能测试。如果你想进行单元测试,你将不得不模拟很多东西,编写复杂的代码,可读性较差。我建议您进行功能测试,因为它更容易。在文章(上面的链接)中,您可以从框架的创建者那里找到有关实践单元与功能的更多信息 @ml59 呃,我发现了问题。这有点愚蠢。我自己发布了一个答案。这并不复杂。我想知道你对此的看法。我的测试可以吗? 【参考方案1】:

好吧,这个问题从一开始就很愚蠢。我错过了传递$user 作为模拟Requestuser 方法的返回值。以下测试通过:

// tests/Unit/LastSeenMiddlewareTest.php

/** @test */
public function doesLastSeenMiddlewareUpdateUsersLastSeenAt()

    $user = Mockery::mock(User::class);

    /* As I'm making a partial mock,
       I don't need to worry about passing
       through the call to the setAttribute method */

    $user->shouldReceive('save')->once();

    $user = $user->makePartial();

    app()->instance(User::class, $user);

    $request = Mockery::mock(Request::class)
        ->shouldReceive('user')
        ->once()
        ->andReturn($user) // I should've passed $user here
        ->getMock();

    $middleware = new LastSeen;

    $middleware->handle($request, function () );

    $this->assertEqualsWithDelta($user->last_seen_at, now(), 1);

【讨论】:

【参考方案2】:

归根结底,middlaware 只是带有 handle 方法的经典 class。 我将像这样进行单元测试:

  public function doesLastSeenMiddlewareUpdateUsersLastSeenAt()
    
        //note: this is using the old factory syntax prior to Laravel 8 https://github.com/laravel/legacy-factories
        $user = factory(User::class)->create(['last_seen_at' => now()->subHour()]);
        
        //login as the user since in your middleware you get the logged user
        $this->actingAs($user);
        
        //get the middleware through the container
        $middleware = app(LastSeen::class);
        
        //call handle method
        $middleware->handle(request(),  function () );
        $this->assertEqualsWithDelta($user->refresh()->last_seen_at, now(), 1);
    

【讨论】:

您正在创建一个真实的用户和用户身份验证。这对单元测试来说不是很糟糕吗?据我了解,对于单元测试,必须隔离一个单元。我的理解错了吗? 你的回答引起了我的注意。为什么要通过容器获取中间件而不直接实例化呢?有什么好处吗? 使用容器获取新实例是一个好习惯。它将自动解决依赖关系。在那种中间件的情况下,你可以让new LastSeen 得到相同的结果 你对我的第一条评论的回答是什么?谢谢。 在单元测试中,我们希望测试一个特定的方法或断言一个值。重点不是测试整个功能。这就是我正在做的事情。您可以模拟或使用工厂。就个人而言,我不喜欢嘲笑所有东西,并且使测试的可读性降低,并且难以为相同的结果进行调试。只需使用更快的方式

以上是关于Laravel - 如何使用 mockery 对更新用户数据的中间件进行单元测试的主要内容,如果未能解决你的问题,请参考以下文章

Laravel Mockery 集成测试

使用 Laravel 和 Mockery

在 Laravel 4 中使用 Mockery 模拟自定义类

Laravel 5 - 使用 Mockery 模拟 Eloquent 模型

使用 Mockery Eloquent 模型模拟 Laravel

Mockery 和 Laravel 构造函数注入