Un vendredi après-midi, l’équipe front nous signale que l’application mobile n’affiche plus les prix des produits. Rien n’a changé côté React Native. En creusant, on retrouve le coupable : un commit qui renommait price_html en formatted_price dans un contrôleur REST maison, sans que personne côté back ne pense à prévenir le front. Les tests PHPUnit passaient tous, verts du premier au dernier. Normal : ils vérifiaient que la route répondait 200, pas la forme exacte de la réponse.
C’est ce type d’incident qui pousse à ajouter une couche de tests de contrat. L’idée n’est pas de remplacer les tests fonctionnels classiques, mais de figer la structure des réponses JSON pour que toute modification incompatible casse la suite de tests avant d’atteindre la production.
Pourquoi un test « 200 OK » ne suffit pas
La plupart des suites de tests sur les routes de l’API REST de WordPress s’arrêtent à vérifier le code de statut et, au mieux, la présence d’une clé ou deux. C’est utile, mais ça laisse passer trois catégories de régressions silencieuses : un champ renommé, un type qui change (une chaîne qui devient un entier), et un champ qui devient optionnel alors que le front le suppose toujours présent. Aucune de ces trois régressions ne fait planter une requête HTTP. Elles cassent simplement l’appelant, souvent bien après le déploiement.
Le test de contrat consiste à décrire, une fois, la forme attendue d’une réponse sous forme de JSON Schema, puis à valider chaque réponse produite par les tests contre ce schéma. Si un champ disparaît ou change de type, la validation échoue immédiatement, avec un message explicite plutôt qu’un bug remonté trois semaines plus tard par un client.

Écrire un schéma pour une route personnalisée
Prenons une route qui expose des fiches « intervenant » pour un site d’événementiel, enregistrée via register_rest_route. On commence par écrire le schéma dans un fichier JSON dédié, versionné dans le dépôt aux côtés du code :
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"required": ["id", "name", "role", "bio", "photo_url"],
"properties": {
"id": { "type": "integer" },
"name": { "type": "string", "minLength": 1 },
"role": { "type": "string" },
"bio": { "type": "string" },
"photo_url": { "type": ["string", "null"], "format": "uri" },
"social_links": {
"type": "array",
"items": { "type": "string", "format": "uri" }
}
},
"additionalProperties": false
}
Le additionalProperties: false est volontaire : il force à documenter tout nouveau champ dans le schéma en même temps qu’on l’ajoute au contrôleur, plutôt que de le laisser filer silencieusement.
Brancher la validation dans PHPUnit
Côté PHP, la bibliothèque justinrainbow/json-schema s’installe facilement via Composer et s’intègre bien dans un test WP_UnitTestCase. On charge le schéma, on décode la réponse, puis on valide :
use JsonSchema\Validator;
public function test_reponse_intervenant_respecte_le_schema() {
$request = new WP_REST_Request( 'GET', '/mon-site/v1/intervenants/42' );
$response = rest_get_server()->dispatch( $request );
$data = json_decode( wp_json_encode( $response->get_data() ) );
$schema = json_decode( file_get_contents( __DIR__ . '/schemas/intervenant.json' ) );
$validator = new Validator();
$validator->validate( $data, $schema );
$this->assertTrue(
$validator->isValid(),
implode( "\n", array_map(
fn( $e ) => sprintf( '[%s] %s', $e['property'], $e['message'] ),
$validator->getErrors()
) )
);
}
Le message d’erreur en cas d’échec liste précisément les propriétés fautives, ce qui évite de rejouer la requête à la main pour comprendre ce qui a changé.
Cas des collections et de la pagination
Pour une route qui retourne une liste, on valide chaque élément du tableau contre le schéma d’un objet unique, et on ajoute un schéma séparé pour l’enveloppe de pagination (total, page, per_page) exposée dans les en-têtes X-WP-Total et X-WP-TotalPages. Ces en-têtes sont accessibles via $response->get_headers() et méritent leur propre assertion, car ils sont souvent oubliés lors d’une refonte de contrôleur.
Générer le schéma plutôt que l’écrire à la main
Sur les routes qui exposent déjà un schema au sens de l’API REST de WordPress (celui que retourne register_rest_route avec l’argument schema, consultable via une requête OPTIONS), on peut réutiliser directement ce schéma plutôt que d’en maintenir un second en doublon. La fonction rest_validate_value_from_schema fournie par le cœur permet même de valider une valeur contre ce schéma natif sans dépendance externe :
- Récupérer le schéma déclaré via
$route->get_item_schema()dans le test - Boucler sur les propriétés attendues et appeler
rest_validate_value_from_schema( $valeur, $schema_propriete, $nom ) - Réserver le JSON Schema externe aux routes maison qui ne déclarent pas de schéma structuré, comme celles construites au-dessus d’un CPT sans
show_in_restcomplet
Cette approche a l’avantage de tester le schéma que le contrat documente vraiment, sans double maintenance.
Intégrer le contrôle à la CI et informer le front
Une fois les tests de contrat en place, on les fait tourner à chaque pull request dans le même job que le reste de la suite PHPUnit. Le vrai gain arrive quand on publie aussi les schémas eux-mêmes comme artefact de build, consultable par l’équipe front : elle peut alors générer ses types TypeScript directement depuis ces fichiers JSON Schema, avec un outil comme json-schema-to-typescript, et détecter les incompatibilités de son côté avant même de tirer la dernière version de l’API.
Un schéma qui vit dans un coin du dépôt et que personne ne relit sert à rien. Le vrai bénéfice vient du jour où le CI refuse de merger une PR parce qu’un champ a disparu, pas de l’existence du fichier en lui-même.
En résumé
Les tests de contrat ne remplacent ni les tests fonctionnels, ni les tests d’intégration classiques : ils ajoutent une garantie précise sur la forme des données échangées avec les consommateurs de l’API. Pour un coût de mise en place modeste, une bibliothèque de validation, un fichier de schéma par route sensible et quelques assertions supplémentaires, on élimine une catégorie entière de régressions qui échappe aux tests habituels. Sur nos projets, c’est ce type de test qui a le mieux justifié son coût : il a détecté trois ruptures de contrat en amont dans les six mois suivant sa mise en place, contre zéro incident du même genre auparavant.