Les collections permettent de regrouper et d'organiser une série de requêtes API, donc si vous avez vos requêtes éparpillées vous pouvez les regrouper facilement dans des collections. Voir depuis la doc officielle: Comment creér votre propre collection
Je m'appuie sur mon API flashcards (Spring Boot) qui expose des endpoints REST pour gérer des catégories et des flashcards.
| Method | Endpoint Category | Endpoint Flascards | Description |
|---|---|---|---|
| GET | /api/categories | /api/flashcards | List all |
| GET | /api/categories/search?name=bash | /api/flashcards/search?question=branch | Search by name/question (word in) |
| GET | /api/categories/{id} | /api/flashcards/{id} | Retrieve by id |
| POST | /api/categories/ | /api/flashcards | Create a new |
| PUT | /api/categories/{id} | /api/flashcards/{id} | Update by id |
| DELETE | /api/categories/{id} | /api/flashcards/{id} | Delete |
Chaque requête "Scripts" renvoie une ou plusieurs réponses

Flashcards API
├── Categories
│ ├── add categories
│ ├── get category by id
│ ├── modify categories
│ ├── get category by id after update
│ ├── get category by name
│ ├── delete categorie
│ └── get categories
├── Flashcards
│ ├── add flashcards
│ ├── get flashcard by id
│ ├── modify flahcards
│ ├── get flashcard by id after update
│ ├── get flashcards by word in question
│ ├── delete flashcard
│ └── get flashcards
└── (hooks de collection vides)

pm.test("Status code is 2xx", function () {
pm.expect(pm.response.code).to.be.within(200, 299);
});
pm.test("Response time is acceptable", function () {
pm.expect(pm.response.responseTime).to.be.below(1000);
});
pm.test("Response is JSON", function () {
pm.response.to.be.json;
});
Ces tests couvrent la disponibilité, une performance acceptable, et le format JSON, prérequis indispensables avant de parler métier
Pour GET /api/flashcards,
on vérifie la structure de la liste
L'objectif CI est de vérifier que le contrat REST
(champ id, question, answer, categoryId) ne soit pas cassé par un refactoring JPA/DTO
pm.test("Flashcards list is an array", function () {
pm.expect(pm.response.json()).to.be.an("array");
});
pm.test("Flashcard object structure is valid", function () {
const json = pm.response.json();
if (json.length > 0) {
pm.expect(json[0]).to.have.property("id");
pm.expect(json[0]).to.have.property("question");
pm.expect(json[0]).to.have.property("answer");
pm.expect(json[0]).to.have.property("categoryId");
}
});
Exemple de recherche:GET /api/flashcards/search?question=remove
pm.test("Search flashcards returns 200", function () {
pm.response.to.have.status(200);
});
pm.test("Response is JSON array", function () {
pm.response.to.be.json;
pm.expect(pm.response.json()).to.be.an("array");
});
pm.test("All flashcards match search term", function () {
const term = pm.request.url.query.get("question").toLowerCase();
const results = pm.response.json();
results.forEach(card => {
pm.expect(card.question.toLowerCase()).to.include(term);
});
});
pm.test("Category created", function () {
pm.expect(pm.response.code).to.be.oneOf([200, 201]);
});
pm.test("Response is JSON", function () {
pm.response.to.be.json;
});
const json = pm.response.json();
pm.test("Category ID is present", function () {
pm.expect(json).to.have.property("id");
pm.expect(json.id).to.be.a("number");
});
pm.environment.set("categoryId", json.id);
{{categoryId}} est ensuite réutilisé dans les requêtes GET/PUT/DELETE
pm.test("Flashcard created", function () {
pm.expect(pm.response.code).to.be.oneOf([200, 201]);
});
const json = pm.response.json();
pm.test("Flashcard ID is present", function () {
pm.expect(json).to.have.property("id");
pm.expect(json.id).to.be.a("number");
pm.environment.set("flashcardId", json.id);
});
Exemple avec: modify flahcards (PUT /api/flashcards/{{flashcardId}})
Ensuite, 'Get flashcard by id after update' vérifie que les valeurs sont bien persistées en base et non juste renvoyées par un echo, côté backend
pm.test("Flashcard updated", function () {
pm.response.to.have.status(200);
});
const json = pm.response.json();
pm.test("Flashcard content updated", function () {
pm.expect(json.question).to.eql("test PUT");
pm.expect(json.answer).to.eql("PUT test");
pm.expect(json.categoryId).to.eql(Number(pm.environment.get("categoryId")));
});
Suivi de 'Get flashcards', à la fin du scénario:
La post-condition s'assure que ce que le DELETE annonce, est bien reflété dans l'état global de l'API, c'est à dire qu'elle confirme que la suppression s'est bien effectuée
pm.test("Flashcard deleted", function () {
pm.response.to.have.status(200);
});
Lancement de 'Get flashcards' GET /api/flashcards/{{categoryId}}:
pm.test("Deleted flashcard is not present anymore", function () {
const list = pm.response.json();
const id = Number(pm.environment.get("flashcardId"));
const found = list.some(fc => fc.id === id);
pm.expect(found).to.eql(false);
});
Les cas négatifs couvrent ce qui n'existe pas mais que l'utilisateur pourrait par exemple, remplir dans la requête via l'url ou un curl
Chaque requête retourne un code d'erreur HTTP et c'est très bien
| GET | /api/categories/{categoryIdNotExist} | → 404 |
| PUT | /api/categories/{categoryIdNotExist} | → 400 ou 404 |
| POST | /api/categories avec body vide | → 4xx |
| GET | /api/flashcards/{flashcardIdNotExist} | → 404 |
| POST | /api/flashcards avec categoryId inexistant | → 4xx |
| PUT | /api/flashcards avec ID invalide | → 4xx |
pm.test("Can't Get: Category Not Found - error 404", function () {
pm.expect([400, 404]).to.include(pm.response.code);
});
pm.test("GET /categories/{categoryId} returns 404 when category does not exist", function () {
pm.response.to.have.status(404);
});
Exemple POST category by invalid name (body vide):
pm.test("Can't add: Category Not Found", function () {
pm.expect([400, 404]).to.include(pm.response.code);
});
pm.test("Returns client error (4xx)", () => {
pm.expect(pm.response.code).to.be.within(400, 499);
});
L'idée est de vérifier que les validations backend (@Valid, contraintes, mapping d'erreurs) ne sont pas supprimées silencieusement par un refactoring
Pour exécuter la collection, j'utilise un environnement versionné, il contient les variables utilisées dans les scénarios
Dans Postman,
→ ces variables sont injectées dans les URLs et les bodies
via la syntaxe {{variable}}
→ et sont mises à jour à la volée, avec pm.environment.set(...)
dans les scripts de tests
→ En local, on utilise le port 8080
Nous avons un auto-chargement de données (5catégories et 5 flashcards/catégorie), donc categoryId vaudra 6 et flashcardId 26
Dans la CI, le script sera utilisé en environement de stress, sur le port 8081
| base-url | http://localhost:8081 |
| categoryId | initialisé à 1 (sera écrasé par le POST “add categories”) |
| flashcardId | initialisé à 1 (sera écrasé par le POST “add flashcards”) |
| wordInQuestion | mot-clé utilisé dans la recherche (remove) |
| categorySearchTerm | terme utilisé pour la recherche de catégories (git) |
| categoryIdNotExist, | IDs “inexistants” pour la collection d'erreurs(7845) |
| flashcardIdNotExist | IDs “inexistants” pour la collection d'erreurs(999) |
Le Collection Runner de Postman permet d'exécuter automatiquement, une série de requêtes (la collection), avec des jeux de données, des scripts de tests, etc.


Le Runner exécute les requêtes ↓ de haut en bas, ce qui permet d'enchaîner: add → get → modify → verify → delete → verify
C'est exactement ce que fera Newman dans la CI
Postman n'est pas un outil de charge professionnel, mais il donne un premier signal rapide avant de passer à JMeter ou k6 pour de vrais tests de performance. Dans Postman, en utilisant le runner, on peut très facilement simuler un mini test de charge, très util en dev avant de passer à JMeter/k6
La dernière brique consiste à brancher ces collections dans la CI,
on exporte les collections (click droit sur la collection > More > Export) dans le dossier postman/ du projet
newman run postman/flashcards.postman_collection.json \
-e postman/local.postman_environment.json \
--reporters cli,junit \
--reporter-junit-export newman-functional.xml
Output du job dans les Actions Github:
Workflows staging et regarder le job functional-tests
Voir le pipeline complet ci-staging
Cette approche permet de réutiliser exactement ce qui a été testé en local, mais de manière automatisée;
Depuis la branche [staging] et, lors de chaque exécution du pipeline