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

How to feature test more complicated cases on Laravel using PHPUnit

我正在为我的项目使用 Laravel,我是 unit/feature 测试的新手,所以我想知道在编写测试时处理更复杂的特性案例的最佳方法是什么?

我们以这个测试为例:

  // 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() 一样,可以很容易地伪造测试用例的答案。这看起来不错,但是:

我会尽力帮助你,当有人从 testing 开始时,这一点都不容易,特别是如果你没有强大的框架(甚至没有框架)全部)。

所以,让我试着帮助你:

  • 区分单元测试和功能测试非常重要。您正确地使用了功能测试,因为您想测试业务逻辑而不是直接 class。
  • 测试时,我个人的建议是始终创建第二个数据库以仅用于测试。它必须始终完全是空的。
    • 因此,要实现此目的,您必须在 phpunit.xml 中定义正确的环境变量,这样当您只进行 运行 测试时,您不必施展魔法来使其工作.
    • 此外,使用 RefreshDatabase 特征。因此,每次您 运行 测试时,它都会删除所有内容,再次迁移您的表并 运行 测试。
  • 您应该始终创建您需要的东西作为 运行 测试的强制性要求。例如,如果您正在测试用户是否可以取消创建的订单 he/she,您只需要有一个 product、一个 user 和一个 invoiceproductuser。您不需要创建 notifications 或与此无关的任何内容。您必须拥有您期望在实际案例场景中拥有的东西,但没有额外的东西,这样您就可以真正测试它是否完全适用于最少的东西。
  • 你可以run seeders if your setup is "big", so you should be using setup方法。
  • 记住 永远不要 模拟核心代码,例如 requestcontrollers 或类似的东西。如果你嘲笑其中任何一个,你就做错了什么。 (一旦你真正知道如何测试,你将通过经验学习这一点)。
  • 写测试名称时,切记不要使用ifmust等类似的措辞,而是使用whenshould。例如,您的测试 testSucceedIfConnectAuthorised 应命名为 testShouldSucceedWhenConnectAuthorised.
  • 这个提示非常个人化:不要在 Laravel 中使用 RepositoryPattern,这是一种反模式。这不是最糟糕的使用方式,但我建议使用 Service class(不要与 Service Provider 混淆,我的意思是 class 是正常的 class,还是叫Service)来达到你想要的效果。但是,你仍然可以 google 关于这个和 Laravel 你会看到每个人都不鼓励这种模式 Laravel。
  • 最后一个提示,Connection::where('id', $id)->first()Connection::find($id) 完全相同。
  • 我忘了补充一点,你应该总是对你的 URL 进行硬编码(就像你在测试中所做的那样),因为如果你依赖 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 指向您的测试数据库(本地)的环境变量之外,您不应该更改任何内容,并且它应该可以在您不更改代码中的任何内容的情况下工作。