如何使用 PHPUnit 在 Laravel 上测试更复杂的情况

Pic*_*ard 2 testing phpunit laravel laravel-5

我在我的项目中使用 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);
  }
Run Code Online (Sandbox Code Playgroud)

所以我们遇到了这样的情况,两个用户之间有一些连接系统。数据库中有一个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();
  }
Run Code Online (Sandbox Code Playgroud)

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

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

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

mat*_*iti 7

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

那么,让我尝试帮助你:

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

因此,为了在您的情况下帮助您,我假设您有一个清晰的数据库(没有表的数据库,RefreshDatabase特征将为您处理这个问题)。

我会让你的第一次测试如下:

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();
}
Run Code Online (Sandbox Code Playgroud)

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