Il n’y a rien de plus frustrant que d’utiliser une API qui ne se comporte pas de la manière qui est décrite dans sa documentation. Nombreux sont les développeurs ayant déjà vécu cette situation. Et pour cause, maintenir une documentation précise et à jour peut s’avérer être une tâche complexe et laborieuse (et ce, même si cette dernière est générée automatiquement). Pour éviter ces désagréments, il est primordial de pouvoir comparer ce qui est décrit dans la documentation avec le comportement réel du code.
Dans le cadre d’une API, une partie de ce travail consiste à s’assurer de la conformité de la documentation OpenAPI. Une solution pouvant être mise en place consiste à intégrer la validation et la vérification de la documentation au sein de tests fonctionnels. L’objectif de cette approche est de vérifier que la requête HTTP effectuée, ainsi que la réponse retournée lors du test correspondent à ce qui est spécifié. Ainsi, si les tests s’exécutent à chaque modification du code, cela permettra de garantir que l’API se comporte bien tel que décrit dans la documentation OpenAPI tout au cours de la vie du projet.
Pour mettre en place ces tests dans un projet PHP, j’utilise généralement la bibliothèque league/openapi-psr7-validator
qui permet de valider des requêtes et réponses HTTP par rapport à une spécification OpenAPI.
Pour faciliter leur intégration dans vos bases de code, je recommande généralement d’utiliser une classe abstraitre pour mutualiser le code de vérification nécessaire et qui sera dédié aux tests de l’API. Cela pourrait ressembler au code suivant:
// ...
use Symfony\Bundle\FrameworkBundle\KernelBrowser;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
abstract class ApiTestCase extends WebTestCase
{
protected static KernelBrowser $client;
protected function setUp(): void
{
parent::setUp();
static::$client = static::createClient();
}
protected function makeRequest(
string $method,
string $uri,
array $parameters = [],
array $files = [],
array $server = [],
?string $content = null,
bool $changeHistory = true,
): Response {
static::$client->request($method, $uri, $parameters, $files, $server, $content, $changeHistory);
$response = static::$client->getResponse();
// TODO: insérer le code de validation de la requête
// TODO: insérer le code de validation de la réponse
return $response;
}
}
Le code précédent est basé sur l’utilisation du framework Symfony, mais sa logique est universelle. L’idée étant d’avoir une méthode dans laquelle on puisse faire un appel HTTP, récupérer la requête HTTP effectuée ainsi que la réponse retournée afin de pouvoir effectuer des contrôles sur ces dernières.
Ajoutons maintenant le code permettant d’effectuer la vérification des données avec la spécification OpenAPI.
// ...
use League\OpenAPIValidation\PSR7\OperationAddress;
use League\OpenAPIValidation\PSR7\ValidatorBuilder;
use Nyholm\Psr7\Request as Psr7Request;
use Nyholm\Psr7\Response as Psr7Response;
abstract class ApiTestCase extends WebTestCase
{
private const OPENAPI_SPECIFICATION_FILE = '/path/to/specification.json';
private static ?ValidatorBuilder $validatorBuilder = null;
// ...
protected function makeRequest(
// ...
bool $enableOpenApiSpecificationRequestCheck = true,
bool $enableOpenApiSpecificationResponseCheck = true,
): Response {
// ...
if ($enableOpenApiSpecificationRequestCheck) {
$this->validateOpenApiRequestSpecification($method, $uri, static::$client->getRequest());
}
if ($enableOpenApiSpecificationResponseCheck) {
$this->validateOpenApiResponseSpecification($method, $uri, static::$client->getResponse());
}
return $response;
}
private function validateOpenApiRequestSpecification(string $method, string $endpoint, Request $request): void
{
$validator = $this->getValidatorBuilder()->getRequestValidator();
$validator->validate(
new Psr7Request(strtolower($method), $endpoint, $request->headers->all(), $request->getContent()),
);
}
private function validateOpenApiResponseSpecification(string $method, string $endpoint, Response $response): void
{
$validator = $this->getValidatorBuilder()->getResponseValidator();
$validator->validate(
new OperationAddress($endpoint, strtolower($method)),
new Psr7Response($response->getStatusCode(), $response->headers->all(), (string) $response->getContent()),
);
}
private function getValidatorBuilder(): ValidatorBuilder
{
return self::$validatorBuilder ??= new ValidatorBuilder()->fromJsonFile(self::OPENAPI_SPECIFICATION_FILE);
}
}
De cette manière, à chaque appel à votre API, vous avez la possibilité de vérifier la documentation qui lui est associée. Vous remarquerez au passage qu’il est possible de désactiver la vérification soit de la requête soit de la réponse. Si la désactivation de la vérification de la réponse peut-être discutable, pouvoir désactiver la vérification de la requête est indispensable pour pouvoir tester les cas d’erreurs avec des appels HTTP invalident (afin de pouvoir tester le comportement d’un client qui effectuerait un mauvais appel).
L’intégration de la validation OpenAPI dans vos tests fonctionnels vous permettra de faire “d’une pierre deux coups”: vous aurez la possibilité d’écrire des tests fonctionnels pour tester le comportement de votre code tout en permettant d’assurer sa cohérence avec votre documentation. Cette approche renforcera la qualité de votre API et évitera des désagréments auprès de vos utilisateurs. N’hésitez pas à adapter cette méthode à votre contexte et les différents frameworks que vous utilisez.