Maximize Page
Tech & DevOps HubEspace Tech & DevOps: Explorez le monde du Dev, du Cloud et des outils DevOps à travers nos articles et discussions Explore the world of development, the cloud and DevOps tools

Création des scénarios de tests (en local)

Date de l'article:12-11-2025
CI/CD Postman Github-Actions Newman
Construction de deux collections Postman; l'une fonctionnelle et l'autre, de gestion d'erreurs afin de valider tous les endpoints de l'API avant l'intégration dans la CI. On y inclut l'exécution via le Runner, les règles de structuration des tests et un test de charge en bonus.
L'objectif est de créer et structurer des collections de tests en y intégrant différents types de requêtes. On organise ces requêtes sous forme de scénarios et ensuite, on exécute ces collections en local via un "Runner" (Postman). On intégre ensuite les jeux de données au projet afin que Newman prenne le relais. En bonus, on test un premier test de charge (postman)
Les scénario des tests
Grâce à Postman, en effectue la validation des endpoints réels, on fait des tests sur le comportement HTTP et la simulation des scénarios définis.
  • Les tests fonctionnels sont utilisés pour les scénarios “happy path” (CRUD complet, recherche, chaînage avec variables)
  • Les tests d'erreur utilisera la collection flashcards_error_cases.postman_collection.json pour gérer les code d'erreur HTTP
  • L'environement local dedié
Côté DevOps, ces séparations apporte plusieurs bénéfices :
  • Lisibilité: chaque collection a un objectif clair (fonctionnel vs robustesse / validations)
  • Stratégie CI: on peut décider de faire échouer le pipeline sur le moindre échec “happy path”, et gérer les tests négatifs dans un job séparé
  • Évolution: les cas d'erreurs évoluent différemment du contrat fonctionnel, les avoir isolés permet de les mettre à jour sans polluer les scénarios principaux
  • Debug : en cas de régression, on sait tout de suite si c'est le comportement nominal qui casse, ou les validations d'entrée / gestion des erreurs
C'est exactement le même principe que séparer des tests d'intégration “happy path” et des tests de robustesse, dans une suite de tests backend

Créer la collection

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

Process:
Cliquez sur "+ > Blank collection" dans la barre latérale,
ou New > Collection, nommez-la, ajoutez les requêtes en cliquant sur "Add requests" ou en glissant des requêtes existantes à l'intérieur de la collection.
Vous pouvez aussi construire directement vos requêtes et choisir/créer la collection au moment de sauvegarder.

API et endpoints utilisés

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

Principes pour des tests Postman “CI-ready
L'objectif n'est pas juste faire des calls HTTP, il s'agit de garantir que la collection puisse tourner telle quelle dans des outils CI comme Newman*, puis dans GitHub Actions
* Newman est le moteur de run en ligne de commande: Il exécute les mêmes collections et environnements, mais en mode headless dans un terminal ou dans la CI

Quelques règles à appliquer
  • Chaque requête doit contenir des assertions minimales (code HTTP, temps de réponse, format JSON)
  • Les requêtes mutantes (POST/PUT/DELETE) doivent être chaînées via des variables d'environnement (categoryId, flashcardId)
  • Aucun ID critique n'est codé en dur: les IDs sont capturés dynamiquement depuis les réponses
  • Les scénarios sont auto-nettoyants: càd: "ce qui est créer dans la collection, est supprimé à la fin"
  • Les tests négatifs (erreurs) vivent dans une collection dédiée "flashcards_error_cases.postman_collection.json" pour séparer “happy path” et “error path

Concrètement :
  • On capture des IDs dynamiquement
  • On valide le schéma JSON
  • On vérifie le contenu fonctionnel
  • On vérifie les codes HTTP attendus
  • On échoue immédiatement si une hypothèse métier n'est pas respectée

Chaque requête "Scripts" renvoie une ou plusieurs réponses

test result

La collection fonctionnelle
flashcards.postman_collection.json
Retrouver cette collection dans le repository de l'application Flashcard
Structure logique de la collection
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)
La collection principale est structurée de façon à refléter la manière dont un pipeline CI va l'exécuter
Dans la plupart des cas, on utilise cette chaîne:
POSTGETPUTGET (verify)DELETEGET (verify 404)
Scénario classique
On démarre avec les catégories car, sans catégorie une flashcard ne peux exister!
  • On crée la catégorie, on vérifie qu'elle existe
  • On la modifie, on vérifie qu'elle à bien été modifiée
  • On la supprime et ensuite on vérifie qu'il la bien été supprimée
Pareil pour les flashcards
- En regardant les endpoints couverts, on sait que l'on devra aussi couvrir la recherche par id et par mot-clé

Les différents types de requêtes
Socle minimal de tests pour chaque requête
Vous pouvez écrire des tests pour vos requêtes API Postman en JavaScript
Depuis l'onglet Scripts et puis Post-response
web api
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


Tests spécifiques par type d'endpoint

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");
    }
});

Tests pour GET search
(flashcards/categories)

Exemple de recherche:GET /api/flashcards/search?question=remove

La requête 'Get flashcards by word in question' utilise la variable d'environnement {{wordInQuestion}} définie dans l'environement local (dev)
Ce test garantit que l'endpoint ne fait pas qu'exister, il filtre les données selon le paramètre de recherche
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);
    });
});

Chaînage avec POST (création)
Exemple avec: POST /api/categories
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

Même principe pour: "add flashcards"
POST /api/flashcards
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);
});
{{flashcardId}} est ensuite réutilisé dans les requêtes GET/PUT/DELETE
Dans les deux cas, l'id est dynamique, ce qui rend la collection portable sur n'importe quel environnement (local, staging, prod)

Tests PUT (mise à jour)

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")));
});

Tests DELETE (et post-conditions)
Exemple delete flashcard avec: DELETE /api/categories/{{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);
});

La collection d'erreurs
flashcards_error_cases.postman_collection.json
Retrouver cette collection dans le repository de l'application Flashcard
Rôle de la collection “error cases”

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

Ce sont, par exemple, des id de catégorie qui n'existe pas,
     un mot non trouvé, une flashcard sans catégorie, etc.

Chaque requête retourne un code d'erreur HTTP et c'est très bien

Exemples de cas couverts:
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
Extrait de tests d'erreur
Exemple GET category by invalid id categorie:
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


L'environement local(dev)
local.postman_environment.json
Retrouver ce fichier dans le repository de l'application Flashcard

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

Les Variables d'environnement (utilisées pour staging)
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)
→ En staging nous n'avons pas de données, donc on démarre la categoryId à 1 et flashcardId à 1

Exécuter la collection avec le Runner

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.

runner
Une fois la collection et l'environnement prêts:
  • Sélectionnez la collection flashcards ᴼ ᴼ ᴼ
  • Cliquez sur Run:

    run collection
  • Choisir l'environnement local et lancer un run complet

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


Bonus: Faire un test de charge avec Postman

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

→ Depuis le runner, avec la collection:
→ Lancer 150 itérations
→ Observer les temps de réponse
→ Des boucles (100, 500, 1000 itérations)
→ Via un jeu de données CSV avec plusieurs lignes
→ Faire l'exécution en parallèle (via Postman Cloud: payant)

Étape suivante: Automatisation via Newman (CLI)

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

Exemple d'exécution via Newman dans un job CI:
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

Dans l'article suivant, nous passerons à la mise en place du test de charge pour pouvoir l'intégrer ensuite dans le pipeline ci-staging

Laissez-moi un commentaire

En postant un commentaire anonyme, vous adhérez automatiquement aux conditions d'utilisation du site.