支持Laravel.io的持续发展 →

验证您的PHP API测试与OpenAPI定义 - Laravel实例

2022年4月1日 阅读时间:12分钟 浏览次数:626

OpenAPI HttpFoundation Testing

OpenAPI 是一种以JSON和YAML格式描述RESTful API的规范,旨在使人类和机器都能理解。

OpenAPI定义与编程语言无关,可以以多种方式使用。

OpenAPI定义可用于文档生成工具以显示API,代码生成工具以生成各种编程语言的服务器和客户端,测试工具以及许多其他用例。

OpenAPI规范

本文演示了如何编写集成测试,比较API响应与OpenAPI 3.0.x定义,以验证前者是否符合后者。

我们将使用 OpenAPI HttpFoundation Testing 包,在一个新的 Laravel 安装中,我们还将使用 L5 Swagger 包生成 Swagger UI 文档。

本文的第一部分将描述问题并进一步阐述背景信息——如果您只是想看代码,欢迎您跳过,直接前往 Laravel示例 部分。

问题所在

现在API很普遍,不错的API都会附带描述端点和如何使用它们的文档。这些文档形式各异,但它们有一个共同点,那就是每当API发生变化时,都必须对文档进行更新。

对许多开发者来说,API文档往往是一个次要考虑的问题——它无聊,有时让人感到厌烦,而且往往缺乏成就感。使用注解来保持代码和文档在同一个地方可能会有所帮助,但这些注解往往很难编写,甚至最愿意的开发者也可能会引入不会必然被审阅者捕获的错误。

在这样的常见场景中,文档和API可能会不一致,导致消费者感到沮丧。

API维护的另一个目标是确保现有的端点能够正常运行——随着时间推移,可能会出现回归,如果没有适当的测试策略,它们可能会被忽视。

一种方法是通过编写控制API行为并自动检测引入的破坏性变化的集成测试来实现这一点。虽然这是一个好的策略,但它并不是无懈可击的——不能保证测试中设置的期望始终与文档完全一致。

我们需要的是一种确保它们保持同步的方法。

解决方案

假设我们有一个用OpenAPI编写的API和一系列集成测试,现在我们想要将后者的期望与前者列出的操作相一致。

虽然使用OpenAPI规范是一个强有力的起点,但它还不足以直接解决问题。

但它的独特之处在于,构建在它之上并不断增多的工具数量在增加,从而将其功能远远超出了文档编写的范畴。

致力于PHP社区并由The PHP League维护的一个工具是OpenAPI PSR-7消息验证器,它是一个用于对OpenAPI定义进行验证的PSR-7 HTTP消息的包。

其想法是将HTTP请求和响应与OpenAPI定义进行比较,并在它们不匹配时返回错误。

换句话说,我们可以使用这个包在集成测试之上添加一个额外的层,比较API响应与我们的API的OpenAPI定义,并在出现不一致时使测试失败。

这里有一张图表来展示API、集成测试和OpenAPI定义之间的关系

Relationship between OpenAPI, API and tests

定义描述了API,测试使用定义来确保API的行为与定义所说的相一致。

突然之间,我们的OpenAPI定义成为了我们的代码和测试的参考——它充当了API的单点真实源。

PSR-7

OpenAPI PSR-7消息验证器包的一个缺点是,正如其名称所示,它只能与PSR-7消息一起工作。

问题是并非所有框架都默认支持这个标准——事实上,很多框架都在底层使用Symfony的HttpFoundation组件,其请求和响应在出厂时并没有实现这个标准。

Symfony 团队已经考虑到这一点,并提供了一个桥梁,可以将 HttpFoundation 对象转换为 PSR-7 对象。这个桥梁只需要一个 PSR-7 和 PSR-17 工厂,他们建议使用Tobias NyholmPSR-7 实现

所有这些片段组合成了一个拼图,由 OpenAPI HttpFoundation Testing 包试图为我们解决,允许开发者在依赖 HttpFoundation 组件的应用程序中使用 OpenAPI 定义来支持其集成测试。

让我们看看如何在 Laravel 项目中使用它,它属于这一类别。

Laravel 示例

该部分的代码也作为GitHub 仓库提供,供您参考。

首先,使用 Composer 创建一个新的 Laravel 9 项目

$ composer create-project --prefer-dist laravel/laravel openapi-example "9.*"

进入项目的根目录,安装一些依赖

$ cd openapi-example
$ composer require --dev osteel/openapi-httpfoundation-testing
$ composer require darkaonline/l5-swagger -W

第一个是前面提到的 OpenAPI HttpFoundation Testing 包,由于其作为测试套件的一部分使用,所以我们将其作为开发依赖项安装。

第二个是L5 Swagger,这是一个流行的包,它将 Swagger PHPSwagger UI 带到 Laravel。实际上,我们在这里不需要 Swagger PHP,因为它是使用 Doctrine 注释来生成 OpenAPI 定义,我们将手动编写自己的 OpenAPI 定义。但是,我们需要 Swagger UI,而这个包方便地将其与 Laravel 集成(-W 选项简单地在这里用于同时更新相关依赖,以避免冲突)。

为了确保 Swagger PHP 不会覆盖 OpenAPI 定义,请将以下环境变量设置在项目的根目录下的 .env 文件中

L5_SWAGGER_GENERATE_ALWAYS=false

storage/api-docs 文件夹中(如果您还没有创建该文件夹)创建一个名为 api-docs.yaml 的文件,并将以下内容添加到其中

openapi: 3.0.3

info:
    title: OpenAPI HttpFoundation Testing Laravel Example
    version: 1.0.0

servers:
    - url: http://localhost:8000/api

paths:
    '/test':
    get:
        responses:
        '200':
            description: Ok
            content:
            application/json:
                schema:
                type: object
                required:
                    - foo
                properties:
                    foo:
                    type: string
                    example: bar

这是一个简单的 OpenAPI 定义,它描述了一个单一操作——对 /api/test 端点的 GET 请求,它应该返回一个包含必需的 foo 键的 JSON 对象。

让我们检查 Swagger UI 是否正确地显示了我们的 OpenAPI 定义。启动 PHP 开发服务器(在项目的根目录中)

$ php artisan serve

在您的浏览器中打开localhost:8000/api/documentation,并在顶部导航栏中将 api-docs.json 替换为 api-docs.yaml(这样做是为了让 Swagger UI 加载 YAML 定义而不是 JSON,因为我们没有提供后者)。

按下 回车 键或点击 Explore – 现在在我们的 OpenAPI 定义应该作为 Swagger UI 文档被渲染。

Swagger UI

展开 /test 端点并尝试它 – 它应该因未实现而失败,并返回 404 Not Found 错误。

让我们现在修复它。打开 routes/api.php 文件,并用这个替换示例路由

Route::get('/test', function (Request $request) {
    return response()->json(['foo' => 'bar']);
});

返回 Swagger UI 选项卡并再次尝试端点 – 它现在应该返回一个成功的响应。

现在是我们写测试的时候了!打开 tests/Feature/ExampleTest.php 并将其内容替换为以下内容

<?php

namespace Tests\Feature;

use Osteel\OpenApi\Testing\ValidatorBuilder;
use Tests\TestCase;

class ExampleTest extends TestCase
{
    /**
     * A basic test example.
     *
     * @return void
     */
    public function testBasicTest()
    {
        $response = $this->get('/api/test');

        $validator = ValidatorBuilder::fromYaml(storage_path('api-docs/api-docs.yaml'))->getValidator();

        $result = $validator->validate($response->baseResponse, '/test', 'get');

        $this->assertTrue($result);
    }
}

让我们来深入分析一下。对于不熟悉Laravel的人来说,$this->get() 是由 MakesHttpRequests 权限所提供的一个测试方法,用于执行对某个端点的 GET 请求,请求的生命周期执行完毕而不会离开应用程序。它返回的响应与如果我们从外部执行相同的请求所获得的响应完全相同。

然后我们使用 Osteel\OpenApi\Testing\ValidatorBuilder 类创建一个验证器,将该YAML定义通过 fromYaml 静态方法喂给验证器(storage_path 函数是一个辅助函数,返回 storage 文件夹的路径,我们将定义放在这里)。

如果我们有一个JSON定义,我们也可以使用 fromJson 方法。此外,这两个方法都接受文件,以及分别接受YAML和JSON字符串。

构建器返回一个 Osteel\OpenApi\Testing\Validator 的实例,我们调用它的 get 方法,传入路径和响应作为参数(《$response` 是一个 Illuminate\Testing\TestResponse 的实例,它是底层 HttpFoundation 对象的包装器,可以通过 baseResponse 公共属性获取)。

上述内容基本上等价于说

我想验证这个响应是否符合对 /test 路径的 GET 请求的 OpenAPI 定义。

它也可以这样写

$result = $validator->get($response->baseResponse, '/test');

这是因为验证器为OpenAPI支持的每个HTTP方法(GETPOSTPUTPATCHDELETEHEADOPTIONSTRACE)提供了一个快捷方法,这使得测试对应操作的响应更加简单。

请注意,指定的路径必须与OpenAPI定义的 paths 中的其中一个完全匹配。

现在您可以运行测试,它应该会成功

$ ./vendor/bin/phpunit tests/Feature

再次打开 routes/api.php,并更改此路由

Route::get('/test', function (Request $request) {
    return response()->json(['baz' => 'bar']);
});

再次运行测试 – 现在它应该会失败,因为响应中包含 baz 而不是 foo,而 OpenAPI 定义指出后者是预期的。

我们的测试现在得到了 OpenAPI 的正式支持!

上述内容显然是为了演示而过于简化的例子,但在实际情况下,一个好的做法是重写 MakesHttpRequests 权限的 call 方法,以便它同时执行测试请求和 OpenAPI 验证。

这样,我们的测试现在可以简化为一行

$this->get('/api/test');

这可以作为一个名为 MakesOpenApiRequests 的新权限来实现,它将“扩展” MakesHttpRequests 权限,并首先调用父类的 call 方法来获取响应。然后它会从URI中确定路径,并在返回之前验证响应是否与 OpenAPI 定义匹配,以便调用测试可以执行任何进一步的断言,如果需要的话。

结论

虽然这种设置是提高API健壮性的很大一步,但它并不是万能的 – 它要求每个端点都有集成测试的覆盖,这不可能以自动化的方式强制执行,最终仍然需要开发者的某些纪律和警觉性。

一开始可能会觉得有点强制,因为维护者基本上被迫保持文档更新,以便编写成功的测试。

然而,附加值在于,这样的文档现在得到了保证,将更加准确,从而让最终用户感到高兴,这也应该会减少开发者沮丧,他们现在花在查找令人头痛的差异上的时间会少得多。

总的来说,使 OpenAPI 定义成为 API 文档和集成测试的唯一真实来源本身就是一种激励,以保持它们的更新。它们自然成为了优先事项,而过去它们只是一些后续考虑的事项。

关于自身维护OpenAPI定义,手动操作可能会显得有些令人望而生畏。注释是一种解决方案,但我个人不喜欢它们,更喜欢直接维护YAML文件。像这个VSCode扩展这样的IDE扩展可以使操作变得容易得多,但如果您对YAML或JSON文件的外观无法忍受,您还可以使用Stoplight Studio这样的工具通过更友好用户界面的方式进行操作。

既然我们在谈论Stoplight,Phil Sturgeon撰写的关于API设计优先 vs. 代码优先的这篇文章是关于API文档的一般起点,并且可能有助于您选择适合您的文档方法。

资源

最后更新1年前。

driesvints, alexanderfalkenberg, dumitriucristian 赞同了这篇文章

3
喜欢这篇文章? 让作者知道,并给他们点赞!
osteel (Yannick Chenot) 英国布莱顿的资深后端开发者。

你可能还喜欢这些文章

2024年3月11日

如何使用Larastan从0到9开发Laravel应用程序

在执行之前发现您的Laravel应用程序中的错误是可能的,多亏了Larastan,它可以...

阅读文章
2024年7月19日

不带特性的标准化API响应

我注意到大多数用于API响应的库都是用特性实现的...

阅读文章
2024年7月17日

通过Discord通知在您的Laravel项目中收集反馈

如何在Laravel项目中创建反馈模块,并在接收到消息时接收Discord通知...

阅读文章

我们想要感谢这些 惊人的公司 对我们的支持

这里放您的标志吗?

Laravel.io

Laravel 门户网站,用于解决问题、知识共享和社区建设。

© 2024 Laravel.io - 版权所有。