🤓 Directeur Technique ( CH Studio)
🎓 Formateur
🤝 Contributeur
Objectif: Normaliser et standardiser la description d'APIs
2009: Swagger specification
2015: Initative OpenAPI
2017: v3.0.0 2021: v3.1.0
Interopérabilité, Automatisation, Fiabilité.
Design-First,Single Source of Truth,Source Control.
// openapi.yaml
openapi: 3.1.0
info:
version: "1.0.0"
title: My Awesome API
servers:
- url: 'http://localhost:8000'
description: Local development server
paths:
/api/users/me:
$ref: users/me.yaml
components:
securitySchemes:
GESSO:
type: apiKey
in: header
name: sso_uid
// openapi.yaml
openapi: 3.1.0
info:
version: "1.0.0"
title: My Awesome API
servers:
- url: 'http://localhost:8000'
description: Local development server
paths:
/api/users/me:
$ref: users/me.yaml
components:
securitySchemes:
GESSO:
type: apiKey
in: header
name: sso_uid
// users/me.yaml
get:
operationId: getCurrentUser
summary: |
Retrieve the logged user details.
security:
- GESSO: [ ]
responses:
'200':
description: |
Successfully retrieved the details of the logged in user
content:
'application/json':
schema:
$ref: 'schema/user.yaml#/User'
// users/me.yaml
get:
operationId: getUserMe
summary: |
Retrieve the logged user details.
security:
- GESSO: [ ]
responses:
'200':
description: |
Successfully retrieved the details of the logged in user
content:
'application/json':
schema:
$ref: 'schema/user.yaml#/User'
// users/me.yaml
get:
operationId: getUserMe
summary: |
Retrieve the logged user details.
security:
- GESSO: [ ]
responses:
'200':
description: |
Successfully retrieved the details of the logged in user
content:
'application/json':
schema:
$ref: 'schema/user.yaml#/User'
// users/schema/user.yaml
User:
type: object
required:
- name
- email
properties:
name:
type: string
example: Jane Doe
updatedAt:
type: string
format: date
example: 2017-07-21
email:
type: string
example: example@ge.com
// users/schema/user.yaml
User:
type: object
required:
- name
- email
properties:
name:
type: string
example: Jane Doe
updatedAt:
type: string
format: date
example: 2017-07-21
email:
type: string
example: example@ge.com
… qui est un projet client aussi !
Un langage descriptif……une suite d'outils !
Créer un faux serveur, des requêtes, des réponses…
Créer un client pour intéragir avec l'API…
Générer de fausses données en utilisant le schéma…
league/openapi-psr7-validator
❤️ PSRs HTTP: Message, Client, Factories…
… conçu comme un middleware …
… framework agnostic …
… basé sur des outils fiables et maintenus.
7
. Message: {Request,Response,ServerRequest}Interface
17
. Factories: {Request,Uri,Stream}FactoryInterface
18
. Client: ClientInterface
symfony/psr-http-message-bridge
use Symfony\Bridge\PsrHttpMessage\Factory\HttpFoundationFactory;
use Symfony\Bridge\PsrHttpMessage\Factory\PsrHttpFactory;
$symfonyRequest = $httpFoundationFactory->createRequest($psrServerRequest);
$psrResponse = $psrHttpFactory->createResponse($symfonyResponse);
use Psr\Http\Client\ClientInterface;
use Psr\Http\Message\{RequestInterface,ResponseInterface};
class HttpKernelClient implements ClientInterface
{
/* constructor */
public function sendRequest(RequestInterface $request): ResponseInterface
{
/* transform RequestInterface to ServerRequestInterface */
$response = $this->kernel->handle(
$this->httpFoundationFactory->createRequest($serverRequest)
);
return $this->psrHttpFactory->createResponse($response);
}
}
use Psr\Http\Client\ClientInterface;
use Psr\Http\Message\{RequestInterface,ResponseInterface};
class HttpKernelClient implements ClientInterface
{
/* constructor */
public function sendRequest(RequestInterface $request): ResponseInterface
{
/* transform RequestInterface to ServerRequestInterface */
$response = $this->kernel->handle(
$this->httpFoundationFactory->createRequest($serverRequest)
);
return $this->psrHttpFactory->createResponse($response);
}
}
use Psr\Http\Client\ClientInterface;
use Psr\Http\Message\{RequestInterface,ResponseInterface};
class HttpKernelClient implements ClientInterface
{
/* constructor */
public function sendRequest(RequestInterface $request): ResponseInterface
{
/* transform RequestInterface to ServerRequestInterface */
$response = $this->kernel->handle(
$this->httpFoundationFactory->createRequest($serverRequest)
);
return $this->psrHttpFactory->createResponse($response);
}
}
use League\OpenAPIValidation\PSR7;
/* Load OpenAPI doc */
$builder = (new PSR7\ValidatorBuilder)->fromYamlFile($yamlFile);
$builder->getRequestValidator()->validate($request);
/* Trigger request and get response */
$operation = new PSR7\OperationAddress('/api/users/me', 'get') ;
$builder->getResponseValidator()->validate($operation, $response);
use League\OpenAPIValidation\PSR7;
/* Load OpenAPI doc */
$builder = (new PSR7\ValidatorBuilder)->fromYamlFile($yamlFile);
$builder->getRequestValidator()->validate($request);
/* Trigger request and get response */
$operation = new PSR7\OperationAddress('/api/users/me', 'get') ;
$builder->getResponseValidator()->validate($operation, $response);
use League\OpenAPIValidation\PSR7;
/* Load OpenAPI doc */
$builder = (new PSR7\ValidatorBuilder)->fromYamlFile($yamlFile);
$builder->getRequestValidator()->validate($request);
/* Trigger request and get response */
$operation = new PSR7\OperationAddress('/api/users/me', 'get') ;
$builder->getResponseValidator()->validate($operation, $response);
use League\OpenAPIValidation\PSR7;
/* Load OpenAPI doc */
$builder = (new PSR7\ValidatorBuilder)->fromYamlFile($yamlFile);
$builder->getRequestValidator()->validate($request);
/* Trigger request and get response */
$operation = new PSR7\OperationAddress('/api/users/me', 'get') ;
$builder->getResponseValidator()->validate($operation, $response);
On veut des tests métiers !
chstudio/raven
1.
Validation par rapport à la documentation
2.
Utilisation de vraies données pour tester
3.
Vérification du comportement de l'API
S'appuie sur fakerphp/faker
…
… résolution de données dynamique …
… ajout des contraintes de validation.
$request = $requestFactory->fromArray([
'uri' => [
'base' => '/api/users/{id}',
'parameters' => [
'{id}' => '<userId()>'
]
],
'method' => 'POST',
'body' => [
'name' => '<name()>',
'creationDate' => '<date("Y-m-d")>',
'office' => '<officeId("LYON")>'
]
]);
$request = $requestFactory->fromArray([
'uri' => [
'base' => '/api/users/{id}',
'parameters' => [
'{id}' => '<userId()>'
]
],
'method' => 'POST',
'body' => [
'name' => '<name()>',
'creationDate' => '<date("Y-m-d")>',
'office' => '<officeId("LYON")>'
]
]);
use CHStudio\Raven\Validator\Expectation\ExpectationFactory;
$requestData = [
'uri' => 'http://myhost.com/api/users/me',
'method' => 'GET',
'statusCode' => 403
];
$expectations = (new ExpectationFactory())->fromArray($requestData);
$request = $requestFactory->fromArray($requestData);
$executor->execute($request, $expectations);
use CHStudio\Raven\Validator\Expectation\ExpectationFactory;
$requestData = [
'uri' => 'http://myhost.com/api/users/me',
'method' => 'GET',
'statusCode' => 403
];
$expectations = (new ExpectationFactory())->fromArray($requestData);
$request = $requestFactory->fromArray($requestData);
$executor->execute($request, $expectations);
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
class OpenApiTest extends WebTestCase
{
/** @dataProvider giveRequests */
public function testThroughOpenApi(array $requestData)
{
$httpClient = new HttpKernelClient(self::createKernel());
/** [...] */
}
public function giveRequests(): Generator
{
yield /** [...] */
}
}
$factory = Factory::fromYamlFile('doc/api/openapi.yaml');
$executor = new Executor(
$httpClient,
$factory->getRequestValidator(),
$factory->getResponseValidator()
);
$request = $requestFactory->fromArray($requestData);
$expectations = $expectationFactory->fromArray($requestData);
try {
$executor->execute($request, $expectations);
} catch (Throwable $e) {
$message = $e->getMessage();
}
static::assertNull($message, $message ?? '');
$factory = Factory::fromYamlFile('doc/api/openapi.yaml');
$executor = new Executor(
$httpClient,
$factory->getRequestValidator(),
$factory->getResponseValidator()
);
$request = $requestFactory->fromArray($requestData);
$expectations = $expectationFactory->fromArray($requestData);
try {
$executor->execute($request, $expectations);
} catch (Throwable $e) {
$message = $e->getMessage();
}
static::assertNull($message, $message ?? '');
$factory = Factory::fromYamlFile('doc/api/openapi.yaml');
$executor = new Executor(
$httpClient,
$factory->getRequestValidator(),
$factory->getResponseValidator()
);
$request = $requestFactory->fromArray($requestData);
$expectations = $expectationFactory->fromArray($requestData);
try {
$executor->execute($request, $expectations);
} catch (Throwable $e) {
$message = $e->getMessage();
}
static::assertNull($message, $message ?? '');
$factory = Factory::fromYamlFile('doc/api/openapi.yaml');
$executor = new Executor(
$httpClient,
$factory->getRequestValidator(),
$factory->getResponseValidator()
);
$request = $requestFactory->fromArray($requestData);
$expectations = $expectationFactory->fromArray($requestData);
try {
$executor->execute($request, $expectations);
} catch (Throwable $e) {
$message = $e->getMessage();
}
static::assertNull($message, $message ?? '');
$factory = Factory::fromYamlFile('doc/api/openapi.yaml');
$executor = new Executor(
$httpClient,
$factory->getRequestValidator(),
$factory->getResponseValidator()
);
$request = $requestFactory->fromArray($requestData);
$expectations = $expectationFactory->fromArray($requestData);
try {
$executor->execute($request, $expectations);
} catch (Throwable $e) {
$message = $e->getMessage();
}
static::assertNull($message, $message ?? '');
/** Exemple d'erreur de validation dans PHPUnit */
✘ Through open api with users.yaml::/api/users/me
┐
├ Unexpected status code 500, expected 200
├ Failed asserting that false is true.
│
╵ /app/tests/Api/OpenApiTest.php:94
┴
/** Exemple d'erreur de validation dans PHPUnit */
✘ Through open api with users.yaml::/api/users/me
┐
├ Unexpected status code 500, expected 200
├ Failed asserting that false is true.
│
╵ /app/tests/Api/OpenApiTest.php:94
┴
/** Exemple d'erreur de validation dans PHPUnit */
✘ Through open api with users.yaml::/api/users/me
┐
├ Data validation failed for property data.attributes.email,
├ invalid value: array (
├ 'name' => 'Jane Doe',
├ 'updatedAt' => '2022-10-14'
├ )
├ Keyword validation failed: Required property 'email' must
├ be present in the object.
├
├ Failed asserting that false is true.
│
╵ /app/tests/Api/OpenApiTest.php:94
┴
/** Exemple d'erreur de validation dans PHPUnit */
✘ Through open api with users.yaml::/api/users/me
┐
├ Data validation failed for property data.attributes.email,
├ invalid value: array (
├ 'name' => 'Jane Doe',
├ 'updatedAt' => '2022-10-14'
├ )
├ Keyword validation failed: Required property 'email' must
├ be present in the object.
├
├ Failed asserting that false is true.
│
╵ /app/tests/Api/OpenApiTest.php:94
┴
/** Exemple d'erreur de validation dans PHPUnit */
✘ Through open api with users.yaml::/api/users/me
┐
├ Data validation failed for property data.attributes.email,
├ invalid value: array (
├ 'name' => 'Jane Doe',
├ 'updatedAt' => '2022-10-14'
├ )
├ Keyword validation failed: Required property 'email' must
├ be present in the object.
├
├ Failed asserting that false is true.
│
╵ /app/tests/Api/OpenApiTest.php:94
┴
Respect du schéma …
… requêtes basé sur des vraies données …
… vérifications spécifiques au projet.
Un projet GitHub avec un README
,
Une utilisation des standards,
Contributions bienvenues 😜.
Tout ne doit pas être documenté …
… un point d'API à la fois …
… enrichir les tests avec les cas particuliers.
▶ Documentation Driven Design
Documenter l'API, écrire les requêtes …… puis coder !
@s_hulard
https://github.com/chstudio/raven