Aller au contenu
English

Garder la documentation exacte

  • Leçon 12
  • 45 min
  • Module 4 : Documenter l'API avec OpenAPI et Swagger UI

Ce que vous saurez faire

Empêchez toute modification accidentelle du contrat de l’API. Un test compare l’API à une copie révisée de son contrat. Un deuxième test envoie l’exemple documenté. Si l’un des deux ne correspond plus, le test échoue avant qu’un client voie le changement.

Avant de commencerCe qu’il faut savoir et avoir sous la mainEnviron 45 minutes

Ce que vous devez savoir

Ce qu’il vous faut sur votre ordinateur

Testé avec Python 3.14.6, FastAPI 0.142.2, pytest 9.1.1 et httpx2 2.13.1 sur macOS.

Le code génère la documentation. Elle change donc quand le code change. C’est utile, sauf quand le changement est une erreur :

  1. Le contrat change sans que personne l’ait décidé. Un « petit nettoyage » renomme model_version en version. La documentation montre le nouveau nom et reste exacte. Mais tous les clients lisent encore l’ancien nom et cessent de fonctionner, comme dans Conception des ressources et des points de terminaison. La documentation était exacte, mais le changement était quand même une erreur.
  2. La documentation et le comportement ne concordent pas. Par exemple, l’API rejette un exemple. Autre cas : le code modifie une réponse dans un gestionnaire sans en informer FastAPI, comme pour la structure 422 dans ce module.

Un test peut détecter les deux problèmes. Pour le premier, conservez une copie révisée du contrat dans le projet et comparez l’API à cette copie. Pour le deuxième, envoyez les exemples documentés et comparez les résultats.

  1. Ajoutez les outils pour travailler sur l’API à requirements.txt :

    requirements.txt
    # Needed to run the API
    fastapi[standard]==0.142.2
    # Needed to work on the API
    pytest==9.1.1
    httpx2==2.13.1

    Le client de test de FastAPI envoie des requêtes à l’application sans serveur. Il utilise le paquet httpx2 pour cela.

  2. Installez-les et mettez à jour le fichier de verrouillage :

    Fenêtre de terminal
    python -m pip install -r requirements.txt
    python -m pip freeze > requirements-lock.txt
  3. Créez les dossiers tests et contract.

Un petit module écrit le document OpenAPI actuel dans contract/openapi.json. Il le met en forme pour faciliter la lecture et la comparaison :

ticket_api/export_contract.py
"""Write the current OpenAPI document to contract/openapi.json."""
import json
from pathlib import Path
from ticket_api.main import app
path = Path("contract/openapi.json")
path.parent.mkdir(exist_ok=True)
path.write_text(json.dumps(app.openapi(), indent=2) + "\n", encoding="utf-8")
print(f"Wrote {path}")

app.openapi() renvoie le même document que /openapi.json, sous forme de dictionnaire. Amira l’exécute une fois. Elle lit le fichier pour vérifier qu’il décrit ce qu’elle veut, puis le conserve dans le projet :

Terminal
python -m ticket_api.export_contract
Sortie
Wrote contract/openapi.json

Désormais, contract/openapi.json est le contrat qu’elle a révisé. Si elle utilise Git, elle en fait un commit (enregistrement des changements). Chaque modification apparaît dans une demande de tirage (pull request), où une personne peut la réviser.

Puis, deux tests :

tests/test_contract.py
import json
from pathlib import Path
from fastapi.testclient import TestClient
from ticket_api.main import app
CONTRACT = Path(__file__).parent.parent / "contract" / "openapi.json"
client = TestClient(app)
def test_openapi_matches_the_reviewed_contract():
current = json.dumps(client.get("/openapi.json").json(), indent=2) + "\n"
reviewed = CONTRACT.read_text(encoding="utf-8")
assert current == reviewed, (
"The API contract changed. If the change is intended, run "
"python -m ticket_api.export_contract and review the difference."
)
def test_documented_example_gives_the_documented_result():
schemas = client.get("/openapi.json").json()["components"]["schemas"]
request_example = schemas["TicketIn"]["examples"][0]
response_example = schemas["Classification"]["examples"][0]
response = client.post("/v1/classify", json=request_example)
assert response.status_code == 200
assert response.json() == response_example
Partie Raison
TestClient(app) Envoie des requêtes à l’application en mémoire, sans serveur ni port. Le module 6 l’utilise pour tous les tests des points de terminaison.
json.dumps(..., indent=2) Compare les documents comme du texte mis en forme. Un échec montre ainsi les lignes modifiées, plutôt que deux longs dictionnaires.
Le message dans assert Indique quoi faire si le changement est voulu. Un échec de test doit expliquer sa cause.
Le deuxième test Prend les exemples dans le document lui-même. Il vérifie donc exactement ce qu’un développeur voit dans /docs.
Terminal
python -m pytest
Sortie (dernières lignes)
tests/test_contract.py .. [100%]
============================== 2 passed in 0.14s ===============================

Pour vérifier le test, Amira fait le « petit nettoyage ». Dans models.py, elle renomme model_version en version dans Classification et dans son exemple. Dans routes.py, elle remplace model_version=classifier.version par version=classifier.version. L’API fonctionne encore et /docs est exact. Le test échoue :

Sortie (abrégée)
E AssertionError: The API contract changed. If the change is intended, run python -m ticket_api.export_contract and review the difference.
E Skipping 4118 identical leading characters in diff, use -v to show
E - "model_version": {
E ? ------
E + "version": {
E "type": "string",
...
FAILED tests/test_contract.py::test_openapi_matches_the_reviewed_contract - A...
========================= 1 failed, 1 passed in 0.13s ==========================

La ligne - représente le contrat révisé et la ligne + représente l’API actuelle. Le renommage est visible. Comme il brise la compatibilité, elle l’annule.

Quand un changement est voulu, par exemple un nouveau champ facultatif, voici les étapes :

  1. Exécutez python -m ticket_api.export_contract pour écrire le nouveau contrat.
  2. Lisez les différences. Avec Git : git diff contract/openapi.json. Sans Git, comparez le fichier à votre dernière copie dans VS Code (File › Compare Active File With…, fichier › comparer le fichier actif avec…).
  3. Décidez s’il brise la compatibilité (module 3). Si oui, il faut une nouvelle version, pas seulement un nouveau fichier.
  4. Relancez les tests. Ils réussissent et le contrat révisé correspond à l’API.

Les pages de documentation sont utiles à toute personne qui crée un client. Elles montrent aussi chaque point de terminaison et chaque champ à un attaquant. La décision dépend du public de l’API :

Public de l’API Documentation en production
Le grand public ou de nombreux développeurs externes Gardez /docs et /openapi.json publics. Ce sont le mode d’emploi du produit.
Votre propre interface utilisateur ou quelques équipes connues Désactivez les pages et donnez plutôt contract/openapi.json aux équipes.
Seulement d’autres services internes de votre entreprise Désactivez-les.

Les désactiver n’est pas une mesure de sécurité. Chaque point de terminaison doit quand même vérifier ses entrées et ses accès. Un attaquant peut trouver les points de terminaison sans documentation. Cela ne fait que retirer une carte. Amira en fait un paramètre, pour que chaque environnement où l’API fonctionne puisse choisir :

ticket_api/main.py (changes)
show_docs = os.environ.get("SHOW_DOCS", "true").lower() == "true"
app = FastAPI(
# ... title, version, description and openapi_tags as before
docs_url="/docs" if show_docs else None,
redoc_url="/redoc" if show_docs else None,
openapi_url="/openapi.json" if show_docs else None,
)

None désactive une page. Avec SHOW_DOCS=false, les trois chemins renvoient 404 dans la structure d’erreur habituelle. L’API fonctionne comme avant.

  1. Installez les outils de test et créez ticket_api/export_contract.py et tests/test_contract.py.
  2. Exécutez python -m ticket_api.export_contract. Ouvrez contract/openapi.json et trouvez le schéma Classification et son exemple.
  3. Exécutez python -m pytest. Les deux tests réussissent.
  4. Renommez model_version en version, comme dans l’exemple détaillé. Lancez les tests. Le test du contrat échoue et montre le renommage.
  5. Annulez le renommage. Relancez les tests. Les deux réussissent.
  6. Remplacez le sujet de l’exemple TicketIn par "Hello" (bonjour), sans modifier l’exemple Classification. Lancez les tests. Quel test échoue maintenant, et pourquoi? Annulez ensuite le changement.
  7. Ajoutez le paramètre SHOW_DOCS. Démarrez le serveur avec SHOW_DOCS=false ($env:SHOW_DOCS = "false" dans PowerShell). Vérifiez que /docs renvoie 404 et que /health fonctionne encore. Redémarrez-le ensuite sans la variable.
Afficher la réponse à l’étape 6

Les deux tests échouent. Le test du contrat échoue parce que l’exemple fait partie du document. Le document a donc changé. Le test de l’exemple échoue parce que "Hello" est classé dans other (autre), ce qui n’est pas la réponse documentée. La documentation montrerait une requête et une réponse qui ne vont pas ensemble.

Le service de soutien demande un nouveau champ de réponse facultatif, explanation. Ce court texte explique pourquoi le classificateur a choisi la catégorie, par exemple "matched: charged, invoice" (correspondances : facturé, facture). Ajoutez-le pour modifier le contrat volontairement :

  1. Ajoutez le champ à Classification, avec une description et une valeur dans l’exemple. Définissez sa valeur dans la route. (Le classificateur peut renvoyer les mots correspondants dans Prediction.)
  2. Lancez les tests et lisez le message d’échec.
  3. Décidez : ce changement brise-t-il la compatibilité? Quelle nouvelle version l’API reçoit-elle?
  4. Mettez à jour le contrat révisé, examinez les différences et relancez les tests.
Afficher la réponse à l’étape 3

Il ne brise pas la compatibilité : les anciens clients ne lisent pas explanation. Tous les champs qu’ils lisent sont encore présents. Le chemin reste donc /v1 et la version passe de 1.0.0 à 1.1.0. Si vous le rendiez plutôt obligatoire dans les requêtes, les anciens clients échoueraient. Ce changement briserait la compatibilité. Retirez le champ avant le prochain module et exportez de nouveau le contrat pour que votre projet corresponde au cours.

Erreur courante

Mettre à jour le contrat pour faire réussir le test

Symptôme : le test du contrat échoue. La correction la plus rapide consiste à exécuter export_contract, puis à relancer les tests. Ils réussissent. Une semaine plus tard, un client cesse de fonctionner.

Cause : le test montre seulement que le contrat a changé. L’exportation accepte tout changement sans qu’on le lise. La copie révisée n’est utile que si quelqu’un la révise.

Solution :

  1. Avant d’exporter, lisez les différences montrées par le test. Dites en une phrase ce qui a changé et pourquoi.
  2. Décidez si le changement brise la compatibilité. Si oui, il faut une nouvelle version du chemin ou un accord avec chaque client, pas seulement un nouveau fichier.
  3. Si vous utilisez Git, gardez la modification de contract/openapi.json dans le même commit que le code qui l’a causée. La personne qui révise verra ainsi les deux.

Vérifiez votre compréhension

Les tests réussissent sur votre ordinateur, mais échouent sur celui d'un collègue en montrant une différence dans le contrat. Aucun de vous n'a changé le code. Qu'est-ce qui pourrait être différent?

Afficher la réponse

Les paquets. Une version différente de FastAPI ou de Pydantic peut représenter les mêmes modèles légèrement différemment dans le document OpenAPI. Par exemple, elle peut ajouter un champ dans un schéma. C’est pourquoi le projet contient un fichier de verrouillage. Installez les paquets à partir de requirements-lock.txt pour que tout le monde ait les mêmes versions. Autre cause : SHOW_DOCS=false dans un terminal, ce qui retire /openapi.json.

Vérifiez votre compréhension

Vérification du module : nommez les deux tests de cette leçon et, pour chacun, un changement qui le fait échouer.

Afficher la réponse

test_openapi_matches_the_reviewed_contract échoue pour toute modification du contrat : champ renommé, nouveau point de terminaison, limite différente, description ou exemple modifié. test_documented_example_gives_the_documented_result échoue quand la requête documentée ne donne plus la réponse documentée. Par exemple : une règle du classificateur modifiée, un nouveau champ obligatoire ou un exemple non valide. Ensemble, ils garantissent qu’une modification du contrat résulte d’une décision. Ils vérifient aussi que les exemples de /docs fonctionnent quand on les envoie sans modification.

Récapitulatif

  • Conservez une copie révisée du document OpenAPI dans le projet et comparez l’API à cette copie dans un test.
  • Comparez du texte mis en forme pour qu’un échec montre exactement quelles lignes ont changé.
  • Testez les exemples documentés en les envoyant, pour que /docs ne montre jamais une requête qui échoue.
  • Quand le test du contrat échoue, lisez les différences et prenez une décision. N’exportez pas seulement pour faire réussir le test.
  • Exposez la documentation en production seulement si les utilisateurs de l’API en ont besoin. La cacher ne constitue pas une protection.

Votre API se documente maintenant elle-même, et la documentation reste exacte. Dans le prochain module, vous utilisez le même document OpenAPI dans Postman.

Leçon suivante : Premières requêtes dans Postman