SSD Nodes Learn 🎉 VPS dès $5.50/mois
Guides Matt ConnorPar Matt Connor

Mocking et tests d’API auto-hébergés sur un VPS

WireMock mocke les dépendances, Hurl teste vos endpoints. Déployez les deux sur un VPS avec Docker Compose, fixtures dans Git et rapports conservés après rebuild.

Deux tâches qui partagent un même dépôt

L’émulation et le test d’API auto-hébergés sont deux tâches différentes. Les traiter comme une seule vous fait perdre une semaine. Un mock server remplace une dépendance que vous ne pouvez pas appeler depuis la CI : un prestataire de paiement, une API partenaire, un service upstream limité en débit ou un service qu’une autre équipe n’a pas encore livré. Un API test runner appelle vos propres endpoints dans un ordre défini et vérifie les réponses. Il réutilise certaines valeurs d’une réponse dans la requête suivante.

Ces deux rôles ne se recouvrent pas. Un mock server n’indique jamais si un test réussit ou échoue. Un test runner ne se prononce pas sur la réponse d’un prestataire de paiement lorsqu’une carte est refusée. La plupart des équipes qui louent déjà un serveur finissent par exécuter un exemplaire de chaque outil. Les deux sont démarrés par le même fichier Docker Compose et relus dans la même pull request.

Pourquoi héberger soi-même le mocking et les tests d’API ?

Vos fixtures contiennent des données structurées comme celles de la production. Le body d’une requête de test est un véritable enregistrement client dont le nom a été modifié, ou dont le nom n’a pas été modifié parce que personne ne l’a vérifié. Les stubs enregistrés sont encore plus problématiques : l’enregistrement par proxy conserve exactement la réponse renvoyée par l’upstream. Un répertoire de stubs créé par enregistrement contient donc des tokens actifs et des adresses e-mail de clients jusqu’à ce que quelqu’un lise chaque fichier. Avec un service hébergé, ces données deviennent l’incident de quelqu’un d’autre et votre divulgation.

La deuxième raison est l’accessibilité réseau. Un service lié à une adresse privée n’est pas accessible depuis un runner hébergé. Le test ne peut donc pas s’exécuter. Chaque solution de contournement a un coût. Publier l’API sur Internet pour pouvoir la tester annule la raison pour laquelle elle était privée. Un tunnel ou une copie publique de staging constitue un autre système à maintenir. Une copie de staging peut aussi diverger de la production entre deux releases. Un runner situé sur le même réseau privé appelle directement le service et ne nécessite rien de tout cela. C’est l’argument pratique en faveur de un runner GitHub Actions auto-hébergé.

Quel serveur mock auto-hébergé devriez-vous utiliser ?

Chacun de ces outils s’exécute dans un conteneur sur un serveur que vous contrôlez. La question importante est de savoir lequel considère quoi comme la source de vérité. Cela détermine si la reconstruction du conteneur ne vous coûte rien ou vous prend tout un après-midi.

  • WireMock conserve chaque stub dans un fichier JSON situé dans le répertoire mappings/, avec les corps de réponse volumineux dans __files/. L’image est wiremock/wiremock, son répertoire racine dans le conteneur est /home/wiremock, et elle peut également fonctionner comme proxy d’enregistrement. Les fichiers stockés sur le disque permettent de versionner le mock dans git comme n’importe quel autre code.
  • Mockoon CLI conserve une API mock complète dans un seul fichier de données JSON. Installez-le avec npm install -g @mockoon/cli et démarrez-le avec mockoon-cli start --data ./data-file.json, ou utilisez l’image mockoon/cli en montant ce fichier en bind mount. L’application desktop modifie le même fichier. Vous pouvez donc concevoir l’API dans l’interface graphique et versionner le résultat sans incompatibilité.
  • MockServer s’exécute depuis l’image mockserver/mockserver et écoute sur le port 1080. Les expectations arrivent via sa propre API REST. Cette approche est pratique depuis le code de test, mais risquée en production : une expectation créée par un appel HTTP disparaît lorsque le conteneur redémarre. Utilisez son fichier d’initialisation JSON pour les stubs qui doivent être permanents.
  • Prism construit le mock à partir de votre document OpenAPI, et non à partir de fichiers de stubs distincts. Installez-le avec npm install -g @stoplight/prism-cli, puis exécutez prism mock openapi.yaml. Dans un conteneur, ajoutez -h 0.0.0.0, car Prism se lie à localhost par défaut et reste sinon inaccessible depuis l’extérieur du conteneur.
  • Microcks est l’option la plus complète : il fournit une interface web qui importe des documents OpenAPI et des collections Postman, puis les expose sous forme de mocks et exécute des tests de contrat. Une installation complète nécessite MongoDB et Keycloak, ainsi que Kafka pour ses fonctionnalités asynchrones. L’image tout-en-un microcks-uber intègre un MongoDB en mémoire, que le projet indique comme adapté à une utilisation éphémère. Considérez donc comme temporaires les éléments créés dans cette interface et conservez les artefacts source dans git.

Quel outil auto-hébergé d’exécution de tests d’API devriez-vous utiliser ?

Le travail consiste ici en une séquence : s’authentifier, créer une commande, la relire, puis vérifier que son état a changé. Il faut donc récupérer une valeur dans une réponse et la réutiliser dans la requête suivante. Un outil incapable de conserver l’état entre les appels est un outil de health check, pas un outil de test d’API.

  • Hurl exécute des fichiers texte contenant des requêtes HTTP depuis un binaire unique. Une section [Captures] extrait des valeurs d’une réponse, une section [Asserts] les vérifie et --test transforme l’ensemble en test runner avec un résumé et un code de sortie. La version 8.0.1 est la version actuelle en août 2026.
  • Bruno CLI exécute un dossier de fichiers .bru. Installez-le avec npm install -g @usebruno/cli, puis lancez-le avec bru run folder --env Local --reporter-junit results.xml. Le format des collections repose volontairement sur des fichiers texte dans un répertoire : les diffs restent donc lisibles lors de la revue.
  • Newman exécute des collections Postman en dehors de Postman : npm install -g newman, puis newman run collection.json -r cli,junit --reporter-junit-export results.xml. Le problème vient du format. La collection est un bloc JSON exporté unique. Les modifications se font donc dans Postman, et le fichier versionné dans git est une copie susceptible de devenir obsolète.
  • Schemathesis effectue un autre type de vérification. Il lit un schéma OpenAPI et génère des cas destinés à produire des réponses que le schéma présente comme impossibles : uvx schemathesis run https://your.api/openapi.json. Il détecte les plantages et les violations du contrat, mais ne connaît aucune de vos règles métier. Il complète donc une suite de tests scriptée au lieu de la remplacer.
  • Hoppscotch auto-hébergé est l’option avec interface web. Il nécessite une instance Postgres. Prenez en compte ce compromis avant de l’installer : les collections sont stockées dans une base de données, pas dans votre dépôt.

Un outil est à éviter. Step CI figure encore dans les comparatifs d’outils et son format de workflow YAML est lisible, mais le dépôt n’a reçu aucun commit depuis août 2024. Un programme placé entre votre CI et votre API est un mauvais endroit pour du code qui n’est plus maintenu.

Placer le serveur mock derrière le pare-feu

La configuration ci-dessous exécute WireMock comme substitut d’un prestataire de paiement. Si le format de fichier Compose ne vous est pas familier, Docker Compose sur un VPS présente les commandes de gestion du cycle de vie utilisées dans cette section.

services:
  mock-payments:
    image: wiremock/wiremock:3.13.2
    command: ["--verbose"]
    volumes:
      - ./mocks/payments:/home/wiremock
    ports:
      - "127.0.0.1:8080:8080"
    restart: unless-stopped

Le préfixe 127.0.0.1: du port est l’élément important. Une déclaration 8080:8080 seule publie le mock sur toutes les interfaces, y compris votre IP publique. Il reste alors accessible même si ufw refuse ce port, car Docker ajoute ses propres règles dans la chaîne DOCKER d’iptables, qui sont évaluées avant les règles INPUT d’ufw. Liez plutôt le service à l’adresse loopback ou à l’adresse d’une interface privée. Le kernel n’accepte alors jamais la connexion depuis l’extérieur.

Votre service à tester pointe ensuite vers le mock. Lorsque le service s’exécute dans le même projet Compose, l’URL de base du mock est http://mock-payments:8080, car Compose résout les noms de service sur son propre réseau. Lorsque le service s’exécute sur l’hôte, utilisez http://127.0.0.1:8080. Définissez cette valeur avec une variable d’environnement, jamais directement dans le code. Sinon, l’URL de test risque d’être déployée en production.

Les stubs se trouvent dans ./mocks/payments/mappings/, à raison d’un fichier JSON par stub.

{
  "request": {
    "method": "POST",
    "urlPath": "/v1/charges",
    "bodyPatterns": [{ "matchesJsonPath": "$.amount" }]
  },
  "response": {
    "status": 201,
    "headers": { "Content-Type": "application/json" },
    "jsonBody": { "id": "ch_test_001", "status": "succeeded", "amount": 4200 }
  }
}

Démarrez le service, puis vérifiez ce qui a réellement été chargé.

docker compose up -d --wait mock-payments
curl -fsS http://127.0.0.1:8080/__admin/mappings

--wait attend que le conteneur indique qu’il est opérationnel. Cela fonctionne parce que l’image WireMock fournit un HEALTHCHECK pour son endpoint /__admin/health. L’appel mappings liste tous les stubs lus par le serveur. Si un stub que vous avez écrit n’apparaît pas dans cette liste, il n’a jamais été chargé. Vérifiez que le fichier se trouve sous mappings/ et non dans la racine montée, puis vérifiez que le JSON est valide.

Lorsqu’une requête arrive et qu’aucun stub ne correspond, WireMock renvoie 404 avec un corps commençant par Request was not matched, suivi d’un diff par rapport au stub le plus proche qu’il contient. Lisez ce diff avant de modifier quoi que ce soit : il indique le champ exact qui diffère. Il s’agit généralement d’un chemin contenant /v1/charge, alors que le stub contient /v1/charges.

Écrivez le test sous forme de séquence, en conservant l’état entre les appels

Les fichiers Hurl sont de simples fichiers texte. Installez le deb depuis les releases du projet.

VERSION=8.0.1
curl --location --remote-name https://github.com/Orange-OpenSource/hurl/releases/download/$VERSION/hurl_${VERSION}_amd64.deb
sudo apt update && sudo apt install ./hurl_${VERSION}_amd64.deb

Une suite qui teste votre propre API avec le mock se trouve dans tests/checkout.hurl.

POST {{base_url}}/orders
Content-Type: application/json
{
  "sku": "ssd-1tb",
  "amount": 4200
}
HTTP 201
[Captures]
order_id: jsonpath "$['id']"

GET {{base_url}}/orders/{{order_id}}
HTTP 200
[Asserts]
jsonpath "$.status" == "paid"
jsonpath "$.charge_id" == "ch_test_001"

Le bloc [Captures] transforme ces requêtes en test d’API, au lieu d’en faire deux requêtes indépendantes. order_id est extrait de la première réponse, puis interpolé dans l’URL de la seconde. L’assertion sur charge_id est l’objectif principal du test : elle prouve que votre service a appelé le fournisseur de paiement et enregistré la réponse reçue. La valeur comparée est celle que vous avez écrite dans le stub WireMock. Un seul fichier couvre ainsi les deux parties du flux.

hurl --test --variable base_url=http://127.0.0.1:3000 \
  --report-junit reports/junit.xml \
  --report-json reports/json \
  tests/

Une exécution réussie affiche une ligne par fichier, puis un récapitulatif.

tests/checkout.hurl: Success (2 request(s) in 61 ms)
Executed files:    1
Executed requests: 2 (30.1/s)
Succeeded files:   1 (100.0%)
Failed files:      0 (0.0%)
Duration:          64 ms

En cas d’échec, error: Assert failure affiche le fichier et le numéro de ligne, puis la valeur obtenue et la valeur attendue. hurl se termine alors avec un code différent de zéro, afin que la CI s’arrête. Si status lit pending alors que vous attendiez paid, votre service n’a pas traité la réponse du mock. Consultez ensuite le journal des requêtes WireMock à l’emplacement /__admin/requests. Il indique si l’appel a atteint le mock.

Déclencher la suite depuis votre propre runner CI

Avec un runner enregistré sur le même serveur, le workflow est court. Le runner est un processus ordinaire sur l’hôte. docker et hurl doivent donc être installés sur cet hôte. Rien n’est hérité d’une image hébergée.

name: api-tests
on: [push]
jobs:
  hurl:
    runs-on: self-hosted
    steps:
      - uses: actions/checkout@v4
      - name: Start the mock
        run: docker compose up -d --wait mock-payments
      - name: Run the suite
        run: hurl --test --variable base_url=http://127.0.0.1:3000 --report-junit reports/junit.xml tests/
      - name: Archive the reports
        if: always()
        run: install -d /srv/api-tests/reports/$GITHUB_SHA && cp -r reports/. /srv/api-tests/reports/$GITHUB_SHA/
      - name: Stop the mock
        if: always()
        run: docker compose down

if: always() est important pour l’étape d’archivage. Sans lui, un échec des tests empêche la copie. Vous perdez donc précisément le rapport que vous vouliez consulter. La copie doit également être placée en dehors du workspace, car le runner nettoie le workspace avant le job suivant et les rapports sont supprimés avec lui.

Conservez les résultats, pas seulement ceux de la dernière exécution

Un fichier XML JUnit par commit répond à une seule question : le test a-t-il réussi ? Il n’indique pas quand un endpoint a commencé à ralentir, car plus rien ne lit ces fichiers une fois que vous cessez de les ouvrir. Pour suivre une tendance, ajoutez une ligne par exécution dans une petite base de données sur le même serveur. Une seule table contenant le SHA du commit, le nom du fichier, le nombre de réussites, le nombre d’échecs et la durée suffit. SQLite en production sur un VPS convient dans ce cas : un seul fichier, aucun processus serveur, et tout l’historique est inclus dans la sauvegarde que vous effectuez déjà. Analysez la sortie --report-json de Hurl plutôt que le XML JUnit, car c’est le format lisible par une machine parmi les deux.

Ce qui doit survivre à la reconstruction d’un conteneur

Les définitions de mocks et les suites de tests sont du code source. Elles doivent se trouver dans un dépôt à côté du service qu’elles décrivent et être modifiées dans la même pull request que celle qui modifie un endpoint. Un stub modifié dans une interface web ou une attente envoyée à MockServer via son API REST au runtime n’existe que dans la mémoire de ce conteneur ou dans la base de données de cet outil. Exécutez docker compose down et tout disparaît. Personne ne s’en aperçoit avant qu’un test commence à réussir pour la mauvaise raison. Si vos dépôts s’exécutent également sur votre propre matériel, un serveur git auto-hébergé permet de conserver les fixtures et le service dans la même zone de confiance.

Passons aux règles pratiques. Épinglez les tags d’image, car latest peut modifier la façon dont votre mock fait correspondre les requêtes sans aucune modification dans votre dépôt. Il est alors très difficile de relier l’échec à sa cause. Montez les répertoires de stubs en lecture seule lorsque l’outil n’a pas besoin d’y écrire. Ne placez jamais les stubs d’un mock dans un volume Docker nommé, car le volume devient alors la source de vérité et la copie dans git devient silencieusement obsolète.

Dernier point, qui piège souvent les utilisateurs. Si vous construisez les stubs en enregistrant le trafic réel via un proxy, lisez chaque fichier généré avant de le valider. Un enregistrement contient exactement ce que l’upstream a renvoyé, y compris les bearer tokens et les adresses e-mail des clients. Le valider place définitivement ces données dans votre dépôt, car git conserve le contenu supprimé dans l’historique.

FAQ

Quelle est la différence entre un mock server d’API et un test runner d’API ?

Un mock server répond aux requêtes. Il remplace une dépendance que vous ne pouvez pas appeler depuis la CI et n’indique jamais si le test réussit ou échoue. Un test runner d’API envoie des requêtes à votre propre service, vérifie les réponses, réutilise certaines valeurs d’un appel dans l’appel suivant et se termine avec un code différent de zéro lorsqu’une assertion échoue. Ces outils répondent à des besoins différents. Une configuration courante exécute les deux en même temps : le runner appelle votre service, tandis que votre service appelle le mock.

Puis-je tester une API interne depuis un runner de CI hébergé ?

Pas sans l’exposer. Un runner hébergé se trouve à l’extérieur de votre réseau et ne peut donc pas atteindre un service lié à une adresse privée. Vous pouvez publier l’API, mettre en place un tunnel ou maintenir une copie de staging publique. Chacune de ces options ajoute un système susceptible de tomber en panne ou de divulguer des données. Un runner situé sur le même réseau privé appelle directement le service. C’est la principale raison pratique pour laquelle les équipes auto-hébergent ce type de tâche.

Où les stubs de mock et les suites de tests d’API doivent-ils être stockés ?

Dans git, à côté du service qu’ils décrivent. Les outils qui stockent les définitions dans des fichiers, comme le répertoire mappings/ de WireMock, le fichier de données de Mockoon, les fichiers Hurl et le répertoire .bru de Bruno, facilitent la revue de code et permettent de reconstruire un conteneur sans coût supplémentaire. Les outils qui stockent les définitions dans une base de données ou une interface web nécessitent une stratégie de sauvegarde et une étape d’export. C’est généralement l’export que l’on oublie, jusqu’à ce que le conteneur ait déjà disparu.

Pourquoi mon mock renvoie-t-il 404 alors que le stub semble correct ?

WireMock ne sert un stub qu’en cas de correspondance exacte. Une requête sans correspondance reçoit 404, avec un corps qui commence par Request was not matched, suivi d’une comparaison avec le stub le plus proche. Cette comparaison nomme le champ qui diffère. Les causes courantes sont une barre oblique finale dans le chemin, un en-tête Content-Type requis par le stub mais absent de la requête du client, urlPath utilisé alors que le stub attend urlPathPattern pour un segment variable, ou un matcher de corps qui ne correspond pas à la charge utile. Vérifiez d’abord /__admin/requests pour confirmer que la requête a bien atteint le mock.

Les mocks sont-ils encore nécessaires si j’ai un environnement de staging ?

Oui, pour deux raisons. Une copie de staging d’un service amont que vous ne contrôlez pas peut tomber en panne et appliquer des limites de débit. Votre suite échoue alors pour des raisons qui n’ont rien à voir avec votre code. Cet environnement ne peut pas non plus produire les réponses que vous devez le plus tester, comme une carte refusée ou un timeout de gateway. Un mock renvoie ces réponses à la demande, à la vitesse du réseau local. Une suite qui prend plusieurs minutes contre un sandbox s’exécute alors en quelques secondes. Conservez staging pour la vérification finale avant la release et utilisez les mocks dans la CI.