如何使用 PHPUnit 在 Laravel 上测试更复杂的案例

Posted

技术标签:

【中文标题】如何使用 PHPUnit 在 Laravel 上测试更复杂的案例【英文标题】:How to feature test more complicated cases on Laravel using PHPUnit 【发布时间】:2021-11-08 00:52:18 【问题描述】:

我在我的项目中使用 Laravel,并且我是单元/功能测试的新手,所以我想知道在编写测试时处理更复杂的功能用例的最佳方法是什么?

我们以这个测试为例:

  // tests/Feature/UserConnectionsTest.php
  public function testSucceedIfConnectAuthorised()
  
    $connection = factory(Connection::class)->make([
      'sender_id'     => 1,
      'receiver_id'   => 2,
      'accepted'      => false,
      'connection_id' => 5,
    ]);

    $user = factory(User::class)->make([
      'id' => 1,
    ]);

    $response = $this->actingAs($user)->post(
      '/app/connection-request/accept',
      [
        'accept'     => true,
        'request_id' => $connection->id,
      ]
    );

    $response->assertLocation('/')->assertStatus(200);
  

所以我们遇到了这种情况,我们在两个用户之间有一些连接系统。由其中一位用户创建的数据库中有一个Connection 条目。现在要使其成功连接,第二个用户必须批准它。问题在于UserController 通过connectionRequest 接受这个:

  // app/Http/Controllers/Frontend/UserController.php
  public function connectionRequest(Request $request)
  
    // we check if the user isn't trying to accept the connection
    // that he initiated himself
    $connection = $this->repository->GetConnectionById($request->get('request_id'));
    $receiver_id = $connection->receiver_id;
    $current_user_id = auth()->user()->id;

    if ($receiver_id !== $current_user_id) 
      abort(403);
    

    [...]
  


  // app/Http/Repositories/Frontend/UserRepository.php 
  public function GetConnectionById($id)
  
    return Connection::where('id', $id)->first();
  

所以我们在测试函数中得到了这个假的(工厂创建的)连接,然后我们不幸的是使用它的假 id 在真实数据库中的真实连接中运行检查,这不是我们想要的:(

研究我发现了创建接口的想法,这样我们就可以根据我们是否正在测试提供不同的方法体。就像这里的 GetConnectionById() 一样,可以很容易地伪造测试用例的答案。这看起来不错,但是:

对于一个人来说,这看起来像是一种开销,除了编写测试之外,我还必须使“真实”代码本身变得更加复杂,以便进行测试。 第二件事,我阅读了 Laravel 文档中关于测试的所有内容,并且没有一个地方提到使用接口,所以我也想知道这是否是解决这个问题的唯一方法和最佳方法问题。

【问题讨论】:

针对真实数据库运行它的代码在哪里?您可以嘲笑它,但从您的问题中我无法真正弄清楚那在哪里。 这是最后一个代码示例 - UserRepository。这是应用程序使用的真实代码,这就是我在真实数据库上工作的代码。第一个代码示例 - UserConnectionsTest.php 是工厂制作不在真实数据库中的假连接的地方。 我倾向于建议您模拟存储库并对功能设置期望以检索和批准连接(如果有),这样您就不需要更改数据库。或者,您可以使用测试数据库或 DatabaseTransactions 特征回滚任何数据库更改 在这种情况下你使用 make() make 不会向数据库添加任何内容,这是为什么呢? 在测试时,您根本不应该针对真实数据库进行测试,而是针对您环境中的测试数据库进行测试,并且许多人更喜欢使用将数据库设置为内存中的 Sqlite 进行测试。 【参考方案1】:

我会尽力帮助你,当有人开始测试这并不容易,特别是如果你没有强大的框架(甚至根本没有框架)。

那么,让我来帮你吧:

区分单元测试和功能测试非常重要。您正确地使用了功能测试,因为您想直接测试业务逻辑而不是类。 当您进行测试时,我个人的建议始终是创建第二个数据库以仅用于测试。它必须一直是完全空的。 因此,要实现这一点,您必须在 phpunit.xml 中定义正确的环境变量,这样您就不必在只运行测试时使用魔法。 另外,使用RefreshDatabase 特征。因此,每次运行测试时,它都会删除所有内容,再次迁移表并运行测试。 您应该始终创建运行测试所需的必要内容。例如,如果您正在测试用户是否可以取消他/她创建的订单,您只需要与productuser 关联一个product、一个user 和一个invoice。您不需要创建notifications 或与此无关的任何内容。您必须拥有您在真实案例场景中期望拥有的东西,但没有额外的东西,这样您就可以真正测试它是否完全适用于最少的东西。 如果您的设置“大”,您可以run seeders,因此您应该使用setup 方法。 记住永远不要模拟核心代码,例如requestcontrollers 或类似的东西。如果你在嘲笑其中任何一个,那么你做错了什么。 (一旦您真正知道如何测试,您将通过经验了解这一点)。 在编写测试名称时,请记住不要使用ifmust 以及类似的措辞,而应使用whenshould。例如,您的测试 testSucceedIfConnectAuthorised 应命名为 testShouldSucceedWhenConnectAuthorised。 这个技巧非常个人化:不要在 Laravel 中使用RepositoryPattern,这是一种反模式。这不是最糟糕的东西,但我建议有一个Service 类(不要与Service Provider 混淆,我的意思是一个普通类,它仍然称为Service)来实现你想要的.但是,您仍然可以 google 一下这个和 Laravel,您会发现每个人都不鼓励在 Laravel 中使用这种模式。 最后一个提示,Connection::where('id', $id)->first()Connection::find($id) 完全相同。 我忘了补充,你应该总是硬编码你的URLs(就像你在测试中所做的那样)因为如果你依赖route('url.name')并且名称匹配但真正的URL/api/asdasdasd,你永远不会测试该 URL 是否是您想要的。所以恭喜那里!很多人不这样做,那是错误的。

所以,为了帮助你,我假设你有一个清晰的数据库(没有表的数据库,RefreshDatabase trait 会为你处理这个问题)。

我会让你的第一个测试是这样的:

public function testShouldSucceedWhenConnectAuthorised()

    /**
     * I have no idea how your relations are, but I hope
     * you get the main idea with this. Just create what
     * you should expect to have when you have this
     * test case
     */
    $connection = factory(Connection::class)->create([
        'sender_id' => factory(Sender::class)->create()->id,
        'receiver_id' => factory(Reciever::class)->create()->id,
        'accepted' => false,
        'connection_id' => factory(Connection::class)->create()->id,
    ]);

    $response = $this->actingAs(factory(User::class)->create())
        ->post(
            '/app/connection-request/accept',
            [
                'accept' => true,
                'request_id' => $connection->id
            ]
        );

    $response->assertLocation('/')
        ->assertOk();

然后,除了指向您的测试数据库(本地)的phpunit.xml 环境变量之外,您不应更改任何内容,并且它应该可以在您不更改代码中的任何内容的情况下工作。

【讨论】:

感谢这真的很有帮助,您还帮助我确认了我自己的一些发现和关于它应该如何工作的想法。我不喜欢模拟一些方法以使测试工作的想法,因为那样它就不会在真实代码上运行。现在我也知道如何处理测试中的数据库部分,感谢您提供有关存储库模式的提示,我也会阅读更多相关信息。 当然!乐意效劳 !我在 Laravel 有 3 - 4 年的测试经验,所以我真的很喜欢它。我无法分享您有很多示例的页面,因为我对此一无所知,但我建议您尝试搜索更多遵循我的指导方针的示例!一旦你了解了如何测试这个,你会非常快,也可以做 TDD,所以你应该 100% 测试你的功能代码!

以上是关于如何使用 PHPUnit 在 Laravel 上测试更复杂的案例的主要内容,如果未能解决你的问题,请参考以下文章

如何在 Laravel 的 PHPUnit 中启用 Redis 连接

如何使用 laravel 和 phpunit 测试文件上传?

如何使用 PHPUnit 在 Laravel 上测试更复杂的案例

如何在 laravel 5.5 中使用 phpunit 测试中间件?

如何使用phpunit在具有动态URI的laravel中测试REST api

如何使用 phpunit 在 Laravel 4 中测试命名空间对象