测试你的 REST APIs

在完成每个控制器后,您都会通过发出请求并期待响应来测试 REST API。正如您所想象的那样,这有时会很方便,但这肯定不是正确的做法。测试应该是自动的,并应尽可能多地覆盖。我们必须考虑一种类似于单元测试的解决方案。

在第 10 章 "行为测试 "中,你将学习到更多端到端测试应用程序的方法和工具,其中包括 REST API。不过,由于 REST API 很简单,我们也可以利用 Laravel 提供的工具添加一些很好的测试。实际上,这个想法与我们在第 8 章 "使用现有的 PHP 框架 "中编写的测试非常相似,都是向某个端点发出请求,并期望得到响应。唯一不同的是我们使用的断言类型(可以检查 JSON 响应是否正常)和执行请求的方式。

让我们为与图书相关的端点集添加一些测试。我们需要数据库中的一些书籍才能对其进行查询,因此我们必须在每次测试前填充数据库,即使用 setUp 方法。请记住,为了使数据库中没有测试数据,我们需要使用 DatabaseTransactions 特质。在 tests/BooksTest.php 中添加以下代码:

<?php

use Illuminate\Foundation\Testing\DatabaseTransactions;
use App\Book;

class BooksTest extends TestCase {
    use DatabaseTransactions;
    private $books = [];
    public function setUp() {
        parent::setUp();
        $this->addBooks();
    }
    private function addBooks() {
        $this->books[0] = Book::create(
            [
                'isbn' => '293842983648273',
                'title' => 'Iliad',
                'author' => 'Homer',
                'stock' => 12,
                'price' => 7.40
            ]
        );
        $this->books[0]->save();
        $this->books[0] = $this->books[0]->fresh();
        $this->books[1] = Book::create(
            [
                'isbn' => '9879287342342',
                'title' => 'Odyssey',
                'author' => 'Homer',
                'stock' => 8,
                'price' => 10.60
            ]
        );
        $this->books[1]->save();
        $this->books[1] = $this->books[1]->fresh();
        $this->books[2] = Book::create(
            [
                'isbn' => '312312314235324',
                'title' => 'The Illuminati',
                'author' => 'Larry Burkett',
                'stock' => 22,
                'price' => 5.10
            ]
        );
        $this->books[2]->save();
        $this->books[2] = $this->books[2]->fresh();
    }
}

正如您在前面的代码中所看到的,我们在数据库中添加了三本书,并在类属性 $books 中也添加了三本书。当我们断言响应有效时,就会用到它们。还请注意 fresh 方法的使用;该方法将我们拥有的模型与数据库中的内容同步。我们需要这样做才能在数据库中插入 ID,因为我们事先并不知道它。

在运行每个测试之前,我们还需要做另一件事:验证客户端。我们需要向访问令牌生成端点发出 POST 请求,发送有效凭证,并存储收到的访问令牌,以便在其余请求中使用。你可以自由选择如何提供凭据,因为有不同的方法。在我们的例子中,我们只提供了一个已知存在于数据库中的客户端测试的凭据,但你可能更喜欢每次都将该客户端插入数据库。用以下代码更新测试:

<?php

use Illuminate\Foundation\Testing\DatabaseTransactions;

use App\Book;

class BooksTest extends TestCase {

    use DatabaseTransactions;

    private $books = [];
    private $accessToken;

    public function setUp() {
        parent::setUp();

        $this->addBooks();
        $this->authenticate();
    }

    //...

    private function authenticate() {
        $this->post(
            'oauth/access_token',
            [
                'client_id' => 'iTh4Mzl0EAPn90sK4EhAmVEXS',
                'client_secret' => 'PfoWM9yq4Bh6rhr8oDDsNZM',
                'grant_type' => 'client_credentials'
            ]
        );
        $response = json_decode(
            $this->response->getContent(), true
        );
        $this->accessToken = $response['access_token'];
    }
}

在前面的代码中,我们使用 post 方法发送 POST 请求。该方法接受一个包含端点的字符串和一个包含参数的数组。发出请求后,Laravel 会将响应对象保存到 $response 属性中。我们可以对其进行 JSON 解码,并提取所需的访问令牌。

是时候添加一些测试了。让我们从一个简单的测试开始:给定一个 ID 请求一本书。ID 用于使用图书的 ID 进行 GET 请求(不要忘记访问令牌),并检查响应是否与预期的一致。请记住,我们已经有了 $books 数组,因此执行这些检查非常容易。

我们将使用两个断言:seekJson,用于比较接收到的 JSON 响应和我们提供的响应;assertResponseOk,我们已经在之前的测试中了解过,它只是检查响应是否有 200 状态代码。将此测试添加到类中:

public function testGetBook() {
    $expectedResponse = [
        'book' => json_decode($this->books[1], true)
    ];
    $url = 'books/' . $this->books[1]->id
        . '?' . $this->getCredentials();

    $this->get($url)
        ->seeJson($expectedResponse)
        ->assertResponseOk();
}

private function getCredentials(): string {
    return 'grant_access=client_credentials&access_token='
        . $this->accessToken;
}

我们使用 get 方法而不是 post 方法,因为这是一个 GET 请求。另外请注意,我们使用了 getCredentials 助手,因为我们必须在每个测试中使用它。要查看另一个示例,让我们添加一个测试,在请求包含给定标题的图书时检查响应:

public function testGetBooksByTitle() {
    $expectedResponse = [
        'books' => [
            json_decode($this->books[0], true),
            json_decode($this->books[2], true)
        ]
    ];

    $url = 'books/?title=Il&' . $this->getCredentials();
    $this->get($url)
        ->seeJson($expectedResponse)
        ->assertResponseOk();
}

前面的测试与前面的测试基本相同,不是吗?唯一的变化是端点和预期响应。其余的测试都将遵循相同的模式,因为到目前为止,我们只能获取图书并对其进行过滤。

为了看到不同的东西,让我们来看看如何测试创建资源的端点。有几种不同的方法,其中一种是先发出请求,然后到数据库检查资源是否已创建。另一种方法,也是我们更喜欢的方法,是首先发送创建资源的请求,然后根据响应中的信息,发送请求以获取新创建的资源。这种方法更可取,因为我们只测试 REST API,不需要知道数据库使用的具体模式。此外,如果 REST API 更改了数据库,测试也会继续通过,而且应该继续通过,因为我们只通过接口进行测试。

借书就是一个很好的例子。测试应首先发送 POST 借书请求,指定图书 ID,然后从响应中提取借书 ID,最后发送 GET 请求,询问借书情况。为了节省时间,你可以将下面的测试添加到已有的 tests/BooksTest.php 中:

public function testBorrowBook() {
    $params = ['book-id' => $this->books[1]->id];
    $params = array_merge($params, $this->postCredentials());

    $this->post('borrowed-books', $params)
        ->seeJsonContains(['book_id' => $this->books[1]->id])
        ->assertResponseOk();

    $response = json_decode($this->response->getContent(), true);

    $url = 'borrowed-books' . '?' . $this->getCredentials();
    $this->get($url)
        ->seeJsonContains(['id' => $response['borrowed-book']['id']])
        ->assertResponseOk();
}

private function postCredentials(): array {
    return [
        'grant_access' => 'client_credentials',
        'access_token' => $this->accessToken
    ];
}

总结

在本章中,你了解了 REST API 在网络世界中的重要性。现在,你不仅能使用它们,还能编写自己的 REST API,从而成为一名资源丰富的开发人员。你还可以将自己的应用程序与第三方 API 相集成,为用户提供更多的功能,让自己的网站更有趣、更有用。

在下一章也是最后一章,我们将介绍单元测试之外的一种测试方法:行为测试,它可以提高网络应用程序的质量和可靠性。