Introduction
. The specification is independent of the programming language we use. Therefore, any client can check the specification and integrate with our API. The specification is a YAML or JSON file.
However, we do not need to write the specification by hand, as there are , which provides an easy-to-use interface:
Therefore, following . This package validates requests and responses to the , which does not implement PSR-7. Therefore, we need an extra package to convert Symfony requests and responses to and from PSR-7: , but , which simulates a web server. Therefore, we will have a client to perform requests, the Symfony\Bundle\FrameworkBundle\KernelBrowser service.
We will write a test for the request to get a book
The test is the following:
<?php
declare(strict_types=1);
namespace rubenrubiob\Tests\Functional\Book;
use rubenrubiob\Tests\Common\Validation\OpenApi\OpenApiResponseAssert;
use rubenrubiob\Tests\Functional\FunctionalBaseTestCase;
use Symfony\Bundle\FrameworkBundle\KernelBrowser;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
use Symfony\Component\HttpFoundation\Response;
use function Safe\json_decode;
use function sprintf;
final class GetBookTest extends WebTestCase
{
private const EXISTING_BOOK_ID = '080343dc-cb7c-497a-ac4d-a3190c05e323';
private const NON_EXISTING_BOOK_ID = '4bb201e9-2c07-4a3a-b423-eca329d2f081';
private const INVALID_BOOK_ID = 'foo';
private const EMPTY_BOOK_ID = ' ';
private const REQUEST_METHOD = 'GET';
private readonly KernelBrowser $client;
private readonly OpenApiResponseAssert $openApiResponseAssert;
protected function setUp(): void
{
parent::setUp();
$this->client = static::createClient();
$this->openApiResponseAssert = new OpenApiResponseAssert();
}
public function test_amb_non_existing_book_retorna_404(): void
{
$url = $this->url(self::NON_EXISTING_BOOK_ID);
$this->client->request(self::REQUEST_METHOD, $url);
$response = $this->client->getResponse();
self::assertSame(Response::HTTP_NOT_FOUND, $response->getStatusCode());
$this->openApiResponseAssert->__invoke($response, $url, self::REQUEST_METHOD);
}
public function test_amb_book_retorna_resposta_valida(): void
{
$url = $this->url(self::EXISTING_BOOK_ID);
$this->client->request(self::REQUEST_METHOD, $url);
$response = $this->client->getResponse();
$responseContent = json_decode($this->client->getResponse()->getContent(), true);
self::assertSame(Response::HTTP_OK, $response->getStatusCode());
self::assertEquals(
[
'id' => self::EXISTING_BOOK_ID,
'title' => 'Curial e Güelfa',
'author' => 'Anònim',
],
$responseContent,
);
$this->openApiResponseAssert->__invoke($response, $url, self::REQUEST_METHOD);
}
private function url(string $bookId): string
{
return sprintf(
'/books/%s',
$bookId
);
}
}
We have two tests:
- One for validating a response with an
404HTTP code. - Another to check a valid response, with a
200HTTP code.
In both cases, we validate the response against the OpenAPI specification with the following line:
$this->openApiResponseAssert->__invoke($response, $url, self::REQUEST_METHOD);
We have to take into account that in this example, we should also test the response with the HTTP code 400. We do not show how to persist the fixtures required.
FunctionalBaseTestCase
To simplify our test classes, we can create a TestCase from which all other functional tests extend from:
<?php
declare(strict_types=1);
namespace rubenrubiob\Tests\Functional;
use rubenrubiob\Tests\Common\Validation\OpenApi\OpenApiResponseAssert;
use Symfony\Bundle\FrameworkBundle\KernelBrowser;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
abstract class FunctionalBaseTestCase extends WebTestCase
{
protected readonly KernelBrowser $client;
protected readonly OpenApiResponseAssert $openApiResponseAssert;
protected function setUp(): void
{
parent::setUp();
$this->client = static::createClient();
$this->openApiResponseAssert = new OpenApiResponseAssert();
}
}
Conclusions
With these functional tests, we tie the implementation to the documentation. It is the best way to always have the documentation up-to-date, as our code will always have to comply with the specification.
Especially if we use a CI/CD pipeline, the code in the production environment will always follow the specification; otherwise, it would not be deployed.
Summary
- We briefly reviewed the OpenAPI standard and how to write its specifications.
- We saw the problem that may arise if we do not comply to the API specification.
- We listed the libraries that test a Symfony
Responseagainst the OpenAPI specification, converting it to the PSR-7 standard. - We implemented a service that implements a Symfony
Response, converts it to PSR-7 and validates against the OpenAPI specification. - We used this service in a functional test and generated a base class to reuse it in all our functional tests.
SOCIAL SHARE CARD GENERATOR