Retour au blog
APIRESTTestingDevOps

Orchestrer et tester des workflows d'API REST : login, token et requêtes chaînées (façon n8n)

Tester une API isolée, tout le monde sait faire. Le vrai sujet, c'est d'enchaîner les étapes — se connecter, récupérer un token, le réinjecter, asserter chaque réponse — et de rejouer ce scénario à la demande, comme un flux n8n. Voici comment je m'y prends.

Publié le 10 juin 20266 min de lecture

Tester un endpoint isolé, c'est facile : un GET /health, un code 200, on passe à autre chose. Le problème, c'est que les vraies API ne s'utilisent jamais endpoint par endpoint. Un client réel s'authentifie, récupère un token, l'envoie dans les requêtes suivantes, crée une ressource, la relit, la supprime. C'est un parcours, pas un appel.

Et c'est précisément là que la plupart des suites de tests sont faibles : elles vérifient des briques, jamais le ciment entre les briques.

Dans cet article, je décris la façon dont j'orchestre et je teste ces parcours — login → token → requêtes chaînées → assertions — avec une logique de flux qui ressemble beaucoup à ce qu'on fait dans n8n : des nœuds, des données qui circulent de l'un à l'autre, et une exécution rejouable.

Le mental model : un graphe, pas une liste

Quand on pense « tests d'API », on imagine une liste de cas indépendants. Quand on pense « orchestration », on imagine un graphe d'étapes où chaque nœud produit une sortie consommée par le suivant.

[POST /auth/login] → token ─┐
                            ↓
                  [GET /me] → userId ─┐
                                      ↓
                          [POST /projects] → projectId
                                      ↓
                          [GET /projects/:id] → assert 200

C'est exactement la mécanique de n8n : un nœud, une sortie, un nœud suivant qui lit cette sortie. La différence, c'est qu'ici l'objectif n'est pas d'automatiser un process métier, mais de valider un parcours d'API de bout en bout — en local, en CI, ou en monitoring de production.

Trois primitives suffisent pour tout modéliser :

  1. La requête — méthode, URL, headers, body.
  2. L'extraction — on lit un morceau de la réponse (token, id) et on le range dans une variable.
  3. L'assertion — on vérifie le statut, un champ, un temps de réponse.

Tout le reste (boucles, conditions, parallélisme) n'est que de l'orchestration au-dessus de ces trois briques.

Étape 1 — Le login et le passage de token

Le token est la donnée qui circule dans 90 % des scénarios. L'erreur classique est de le copier-coller à la main entre deux requêtes. On veut au contraire l'extraire une fois et le réinjecter automatiquement.

En brut, avec curl et jq, le principe tient en deux lignes :

TOKEN=$(curl -s -X POST https://api.exemple.com/auth/login \
  -H "Content-Type: application/json" \
  -d '{"email":"demo@exemple.com","password":"motdepasse"}' | jq -r '.accessToken')

curl -s https://api.exemple.com/me \
  -H "Authorization: Bearer $TOKEN" | jq

Ça marche pour un test ponctuel, mais ça ne passe pas à l'échelle : pas d'assertions, pas de réutilisation, illisible dès qu'il y a cinq étapes. On monte donc d'un cran.

Étape 2 — Modéliser le scénario comme une donnée

Ma règle : un scénario est une donnée, pas du code. On le décrit de façon déclarative, et un petit runner l'exécute. Ça le rend versionnable, diffable, et exécutable aussi bien en local qu'en CI.

# scenario.login-and-create.yaml
name: "Login puis création de projet"
baseUrl: "https://api.exemple.com"

steps:
  - id: login
    POST: /auth/login
    body:
      email: "demo@exemple.com"
      password: "{{ env.DEMO_PASSWORD }}"
    expect:
      status: 200
    capture:
      token: "$.accessToken"

  - id: me
    GET: /me
    headers:
      Authorization: "Bearer {{ token }}"
    expect:
      status: 200
    capture:
      userId: "$.id"

  - id: createProject
    POST: /projects
    headers:
      Authorization: "Bearer {{ token }}"
    body:
      name: "Projet de test"
      ownerId: "{{ userId }}"
    expect:
      status: 201
    capture:
      projectId: "$.id"

  - id: readBack
    GET: /projects/{{ projectId }}
    headers:
      Authorization: "Bearer {{ token }}"
    expect:
      status: 200
      body:
        name: "Projet de test"

On retrouve les trois primitives : expect (assertions), capture (extraction via JSONPath), et l'interpolation {{ ... }} qui fait circuler les données entre nœuds. Le secret reste hors du fichier, dans env.

Étape 3 — Un runner minimal en TypeScript

Pas besoin d'une usine à gaz. Un runner séquentiel qui maintient un contexte partagé fait déjà 95 % du travail. Voici le cœur, volontairement réduit à l'essentiel :

type Step = {
  id: string;
  method: "GET" | "POST" | "PUT" | "DELETE";
  path: string;
  headers?: Record<string, string>;
  body?: unknown;
  expectStatus?: number;
  capture?: Record<string, string>;
};

const interpolate = (input: string, ctx: Record<string, unknown>) =>
  input.replace(/\{\{\s*([\w.]+)\s*\}\}/g, (_, key) =>
    String(key.split(".").reduce((o: any, k: string) => o?.[k], ctx) ?? "")
  );

async function runScenario(baseUrl: string, steps: Step[], ctx: Record<string, unknown> = {}) {
  for (const step of steps) {
    const url = baseUrl + interpolate(step.path, ctx);
    const headers = Object.fromEntries(
      Object.entries(step.headers ?? {}).map(([k, v]) => [k, interpolate(v, ctx)])
    );

    const res = await fetch(url, {
      method: step.method,
      headers: { "Content-Type": "application/json", ...headers },
      body: step.body ? interpolate(JSON.stringify(step.body), ctx) : undefined,
    });

    if (step.expectStatus && res.status !== step.expectStatus) {
      throw new Error(`[${step.id}] attendu ${step.expectStatus}, recu ${res.status}`);
    }

    const json = await res.json().catch(() => ({}));
    for (const [name, path] of Object.entries(step.capture ?? {})) {
      ctx[name] = path.replace(/^\$\./, "").split(".").reduce((o: any, k) => o?.[k], json);
    }

    console.log(`OK ${step.id} -> ${res.status}`);
  }
  return ctx;
}

Le ctx est le fil rouge : chaque étape y écrit ses captures, chaque étape suivante y lit ses variables. C'est exactement le « passage de données entre nœuds » de n8n, en 30 lignes. À partir de là, on ajoute ce dont on a besoin : retries, timeout par étape, assertions sur le body, exécution en parallèle des branches indépendantes.

Étape 4 — Le brancher dans la CI

Un scénario qui ne tourne que sur ma machine ne sert à rien. L'intérêt, c'est de le rejouer à chaque push. Avec GitLab CI, ça tient en quelques lignes :

api-e2e:
  stage: test
  image: node:22-alpine
  variables:
    DEMO_PASSWORD: "$DEMO_PASSWORD"
  script:
    - npm ci
    - npm run scenario -- scenario.login-and-create.yaml
  rules:
    - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'

Le même fichier de scénario sert alors trois usages : debug local, test E2E en CI, et — si on le planifie toutes les X minutes contre la prod — monitoring de parcours. Une seule définition, trois cycles de vie.

Les pièges que j'ai appris à éviter

  • Ne jamais coder les secrets en dur. Mot de passe, clé d'API : toujours via l'environnement. Un scénario doit pouvoir être commité sans rougir.
  • Isoler les données de test. Si l'étape crée un projet, prévoir l'étape de nettoyage (ou un DELETE final). Sinon la 50e exécution pollue la base.
  • Asserter le contrat, pas le contenu volatil. Vérifier status et la forme de la réponse, pas un id qui change à chaque run.
  • Surveiller le temps de réponse. Une étape qui passe de 80 ms à 900 ms est un bug silencieux que seul un seuil d'assertion détecte.
  • Garder les étapes idempotentes quand c'est possible. Un parcours rejouable est un parcours utilisable en monitoring.

Faut-il réinventer la roue ?

Non — et c'est important de le dire. Des outils font déjà très bien ça : Bruno et Hoppscotch côté open source et Git-native, Testfully ou Checkly côté hébergé pour le monitoring de parcours. Si vous voulez juste avancer, partez de là.

Comprendre la mécanique sous-jacente reste néanmoins payant : ça vous permet de débrancher l'outil quand il vous limite, d'écrire un runner sur-mesure quand votre stack a des contraintes particulières (auth maison, signatures, webhooks à intercepter), et surtout de raisonner en flux plutôt qu'en cas isolés. C'est ce changement de modèle mental — du test unitaire vers l'orchestration de parcours — qui fait la différence sur une API sérieuse.


Vous avez une API REST à fiabiliser, ou un parcours critique (auth, paiements, webhooks) à mettre sous monitoring ? C'est typiquement le genre de mission sur laquelle j'interviens — écrivez-moi.