20 - Testing : tester ton API sans devenir fou
Ce que tu vas apprendre
- Les outils pour tester manuellement (curl, httpie, Postman)
- Comment écrire des tests automatises pour tes endpoints
- Le contract testing pour vérifier la conformité a la spec
- Le load testing pour anticiper la montee en charge
- La gestion des environnements de test
Prerequisites
Avoir lu l'article sur la documentation. Savoir écrire des tests unitaires dans ton langage. Avoir une API REST a tester.
J'ai longtemps teste mes APIs en faisant des requêtes depuis le front. Je cliquais sur un bouton, j'ouvrais les devtools, je regardais la réponse. Quand ca cassait, je mettais un console.log cote serveur. C'est la méthode la plus lente et la moins fiable qui existe. Le jour ou j'ai écrit mes premiers tests d'intégration, j'ai gagne des heures par semaine.
Tests manuels : les outils
curl
L'outil universel. Installe partout, scriptable, mais la syntaxe est penible :
bashcurl -X POST https://api.example.com/products \
-H "Content-Type: application/json" \
-H "Authorization: Bearer eyJ..." \
-d '{"name": "Burger", "price": 1200}'
httpie
curl pour les humains. La syntaxe est lisible :
bashhttp POST api.example.com/products \
name="Burger" price:=1200 \
Authorization:"Bearer eyJ..."
Les := indiquent une valeur non-string (nombre, booleen). Les = sont des strings. httpie ajoute automatiquement Content-Type: application/json.
Postman / Bruno
Postman est l'outil graphique de référencé. Tu créés des collections de requêtes, tu définis des variables d'environnement, tu ecris des tests en JavaScript. L'inconvenient : c'est un SaaS qui pousse vers un abonnement payant.
Bruno est l'alternative open source que je préféré. Les requêtes sont stockees dans des fichiers texte dans ton repo Git. Pas de compte, pas de cloud, tout est local.
Tests automatises
Le vrai gain, c'est l'automatisation. Pour chaque endpoint, teste le cas nominal et les cas d'erreur :
typescriptimport { describe, it, expect, beforeAll } from "vitest";
const BASE_URL = "http://localhost:3000";
describe("POST /products", () => {
let token: string;
beforeAll(async () => {
const res = await fetch(`${BASE_URL}/auth/login`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email: "admin@test.com", password: "test123" })
});
const data = await res.json();
token = data.token;
});
it("cree un produit avec des donnees valides", async () => {
const res = await fetch(`${BASE_URL}/products`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`
},
body: JSON.stringify({ name: "Burger", price: 1200 })
});
expect(res.status).toBe(201);
const product = await res.json();
expect(product.name).toBe("Burger");
expect(product.id).toBeDefined();
});
it("retourne 422 si le prix est negatif", async () => {
const res = await fetch(`${BASE_URL}/products`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`
},
body: JSON.stringify({ name: "Burger", price: -1 })
});
expect(res.status).toBe(422);
});
it("retourne 401 sans token", async () => {
const res = await fetch(`${BASE_URL}/products`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name: "Burger", price: 1200 })
});
expect(res.status).toBe(401);
});
});
Avec Elysia ou Hono, tu peux tester sans lancer de serveur HTTP grâce à app.request() :
typescriptimport { app } from "../src/app";
it("liste les produits", async () => {
const res = await app.request("/products");
expect(res.status).toBe(200);
const body = await res.json();
expect(body.data).toBeInstanceOf(Array);
});
C'est plus rapide et plus simple que de démarrer un vrai serveur. J'utilise cette approche sur paltemps.fr pour tous les tests d'API.
Contract testing
Le contract testing vérifié que ta réponse est conforme au schema OpenAPI. Si ta spec dit que GET /products retourne un tableau d'objets avec id: number et name: string, le test echoue si la réponse contient un champ inattendu ou s'il en manque un.
typescriptimport SwaggerParser from "@apidevtools/swagger-parser";
import Ajv from "ajv";
const spec = await SwaggerParser.dereference("./openapi.yaml");
const ajv = new Ajv();
it("GET /products respecte le schema", async () => {
const res = await app.request("/products");
const body = await res.json();
const schema = spec.paths["/products"].get.responses["200"]
.content["application/json"].schema;
const valid = ajv.validate(schema, body);
expect(valid).toBe(true);
});
Ca attrape les derives entre la spec et l'implementation. Un dev ajoute un champ internal_notes sans mettre à jour la spec ? Le test echoue.
Load testing
Une API qui marche pour 10 utilisateurs ne marche pas forcement pour 1000. k6 est mon outil préféré pour le load testing :
javascript// load-test.js
import http from "k6/http";
import { check, sleep } from "k6";
export const options = {
stages: [
{ duration: "30s", target: 50 }, // monte a 50 users
{ duration: "1m", target: 50 }, // maintient 50 users
{ duration: "10s", target: 0 } // redescend
]
};
export default function () {
const res = http.get("http://localhost:3000/products");
check(res, {
"status 200": (r) => r.status === 200,
"latence < 200ms": (r) => r.timings.duration < 200
});
sleep(1);
}
bashk6 run load-test.js
k6 te donne les p50, p90, p99 de latence, le debit, et les erreurs. Tu decouvres vite ou sont les bottlenecks : une requête SQL lente, un N+1, un endpoint qui ne gere pas la concurrence.
Environnements de test
Ne teste jamais contre la prod. Mais ne teste pas non plus contre une base de donnees partagee ou les donnees changent entre les runs.
Stratégie 1 : base de donnees dédié par run. Chaque exécution de tests créé un schema ou une base temporaire, lance les migrations, seed les donnees, et nettoie apres.
Stratégie 2 : transactions rollback. Chaque test s'exécuté dans une transaction qui est rollback a la fin. Les donnees ne persistent jamais.
typescriptbeforeEach(async () => {
await db.execute("BEGIN");
});
afterEach(async () => {
await db.execute("ROLLBACK");
});
Stratégie 3 : testcontainers. Un conteneur Docker avec une base de donnees fraiche pour chaque suite de tests. Plus lourd mais complètement isole.
Résumé
- Utilise httpie ou Bruno pour les tests manuels, curl si tu dois scripter
- Ecris des tests automatises pour chaque endpoint : cas nominal, erreurs, authentification
- Le contract testing garantit que ta réponse respecte la spec OpenAPI
- Le load testing avec k6 revele les problèmes de performance avant la prod
- Isole tes donnees de test avec des transactions rollback ou des bases temporaires
Article précédent : Documentation Article suivant : Webhooks