<
Media
>
Article

Mockin' on API's Door ?

7 min
12
/
06
/
2024

Les tests sont importants dans la vie d'un logiciel.

Nombreuses sont les applications qui aujourd'hui, réalisent des appels HTTP(S) vers d'autres applications, qu'elles soient externes au Système d'Information ou internes. Il est généralement assez difficile de tester ces appels, et en général la solution priviligiée est l'utilisation de bouchon (ou mock).

Chacun y va de sa propre solution : écrire son propre mock, utiliser une instance de mock sur étagère (plugin, librairie) ou mocker directement la couche Service réalisant les appels HTTP.

Développer son propre mock a le bénéfice de pouvoir facilement le maintenir et le faire évoluer pour coller au plus près des besoins. Mais ça reste vrai seulement tant que l'équipe de développement garde la connaissance de ce dernier. Il comporte également certains inconvénients, comme gérer son déploiement ou encore le besoin de le considérer comme une application à part entière (tests, qualité, maintenance...).

Mocker les services via l'utilisation de librairies telles que Mockito, est une bonne solution au sein des tests unitaires pour isoler une unité de code (classe, méthode) et afin de tester son comportement en dehors de son environnement réel. Mais ce genre de solutions devient beaucoup moins intéressante dans le cadre des tests d'intégration, lors desquels nous souhaiterons tester également l'interaction entre différentes briques logicielles : base de données, gestionnaire d'identité, application logicielle externe...

Intéressons-nous donc aux tests d'intégration, aux solutions existantes de mocks pour Java - et plus particulièrement de mocks d'API HTTP - et comparons-les pour nous donner la capacité de bien choisir la solution la mieux adaptée à un besoin.

Le but n'est pas ici de faire un descriptif complet des fonctionnalités de chacune des solutions de mock que nous allons présenter, ni de lister toutes les solutions pour mocker une API HTTP, mais de découvrir quelles sont les solutions existantes et de se faire une idée de leurs points forts.

Intérêts du mock

Les intérêts d'utiliser un mock pour simuler une API HTTP sont nombreux.

  • En utilisant un mock d'API, l'équipe de développement peut travailler de manière indépendante, sans avoir à attendre que l'API réelle soit finalisée et/ou disponible. Ce qui peut accélérer le processus de développement.
  • En simulant le comportement d'une API réelle, nous pouvons reproduire des scénarios spécifiques pour le débogage, pour les tests, ce qui facilite l'identification et la correction des erreurs dès le début du processus de développement.
  • Un mock permet de développer et de tester une application même sans accès à l'API, ce qui est particulièrement utile dans les environnements de développements où l'accès à une API peut être inexistant.
  • Un des intérêts majeurs de l'utilisation d'un mock est d'avoir le contrôle total sur les réponses de l'API simulée, ce qui permet de simuler des cas d'utilisation spécifiques ou des erreurs pour tester la réaction de l'application dans différentes situations.
  • Enfin, utiliser un mock pendant le développement permet de réduire les coûts associés à l'utilisation de l'API réelle, surtout si celle-ci implique des frais d'utilisation ou de transaction.

Les solutions pour une application Java

Les solutions sont (très) nombreuses, comme nous avons pu le mentionner lors du talk "API, tu te mock de moi", nous ne pourrons donc pas toutes les comparer mais nous nous attarderons ici sur trois solutions dites "As Code" : MockServer, Wiremock et OpenApi Mock.

Aujourd'hui, Docker est partout, et Testcontainers est en train de s'imposer comme LE framework de "test dependencies as code". Nous prendrons donc comme hypothèse que nos tests utilisent cette solution. Nous prendrons également comme hypothèse que notre application est basée sur Spring Boot (en version 3.X).

Une application de test

Vous trouverez ici une application Spring Boot qui contient trois tests d'intégration correspondant à chacune des solutions de mock que nous allons aborder.

Cette application de gestion de tâches appelle un service météo externe pour enrichir l'expérience de l'utilisateur en lui fournissant des informations météorologiques pertinentes pour chaque tâche qu'il crée. Cela permet à l'utilisateur de mieux planifier et d'adapter ses activités en fonction des conditions météorologiques attendues.

MockServer

MockServer est un des outils de simulation d'API HTTP(S).

Il peut retourner des réponses spécifiques en fonction des requêtes qu'il reçoit. Il peut faire office de proxy en modifiant potentiellement les requêtes et/ou les réponses. Il peut également faire les deux : proxy pour certaines requêtes et mock pour d'autres.

Nous nous focaliserons ici sur la fonctionnalité de mock.

Pour cela, il se base sur la configuration d'_expectations_ (attentes) afin de connaître quelle(s) action(s) il doit effectuer en fonction des requêtes qu'il reçoit.  
Pour créer une expectation, il faut définir un request matcher et la réponse qui doit être retournée.  
Lorsque MockServer reçoit une requête, il va alors rechercher l'expectation qui correspond et retourner la réponse attendue.

Plusieurs façons de l'utiliser sont possibles : via le "Client API", via le plugin Maven, en tant que conteneur Docker seul, via JUnit (en versions 4 et 5), en tant que TestExecutionListener (Spring), ou encore via TestContainer en tant que conteneur Docker.

Pour utiliser MockServer via Testcontainers, rien de plus simple, il suffit de suivre les étapes suivantes :

  • Ajout des dépendances <span class="css-span">org.mock-server:mockserver-netty</span> et <span class="css-span">org.testcontainers:mock-server</span>
  • Annotation de la classe de test avec <span class="css-span">@Testcontainers</span>
  • Déclaration d'un container "mockServer" dans la classe de test :

<pre><code>@Testcontainers
class MockerServerIntegrationTest{

   @Container
   private static final MockServerContainer mockServerContainer = new MockServerContainer(DockerImageName.parse("mockserver/mockserver:5.15.0"));

     [...]
}</code></pre>

  • Création d'un <span class="css-span">MockServerClient</span>
  • Envoi de l'expectation au MockServer (au sein de la méthode de test par exemple) qui retourne un tableau d'<span class="css-span">Expectations</span>:

<pre><code>
   [...]

   // Expectation
   Expectation[] expectations = mockServerClient.when(
       request()
         .withMethod("GET")
         .withPath("/my_path")
   )
   .respond(
       response()
         .withStatusCode(200)
         .withBody("...")
   );

   [...]
</code></pre>

  • Enfin, il suffit de requêter notre application pour vérifier qu'elle utilise bien le mock défini et que la réponse correspond à celle attendue.

<pre><code>
   [...]

   given()
     .contentType(ContentType.JSON)
     .when()
     .get("/myApplicationPath")
     .then()
     .statusCode(200)
     .body("...");
</code></pre>

MockServer peut également utiliser une spécification OpenAPI v3 via les paramètres "specUrlOrPayload" permettant de définir où trouver la spécification et "operationsAndResponses" permettant entre autres choses de définir le statut de la réponse en fonction de l'operationId. Par exemple :

<pre><code>
{
 "specUrlOrPayload": "/my_path/my_spec.yaml",
 "operationsAndResponses": {
     "getMyPath": "200",
     "deleteMyPath": "403"
 }
}
</code></pre>

Notre exemple d'utilisation de MockServer est disponible ici.

Vous êtes restés sur votre faim ?

Vous avez un doute sur le fait que votre application a bien fait appel à MockServer ? Don't panic, il vous permet de vérifier vos appels et le nombre de fois qu'il a reçu une requête (nombre exact, minimum, maximum, jamais...) via la commande :

<pre><code>
 mockServerClient.verify(expectations[0].getId(), VerificationTimes.exactly(2));
</code></pre>

MockServer est également capable de s'appuyer sur des templates de réponses (Mustache, Velocity...).

Comme mentionné plus tôt, MockServer offre également des fonctionnalités de proxying HTTP/HTTPS, ce qui signifie qu'il peut agir comme un proxy pour les requêtes HTTP/HTTPS et les rediriger vers des serveurs réels tout en enregistrant les interactions.

Vous avez beaucoup de tests ? Vraiment beaucoup ? Et vous avez peur que MockServer ne tiennent pas la charge ? Vous avez raison, c'est avant tout une application logicielle, mais rassurez-vous, il est clusterisable. Certes avec quelques contraintes :

  • même port d'écoute pour chaque instance : utilisez alors la commande <span class="css-span">replicas</span> de docker.
  • file-system partagé entre toutes les instances : créez un volume.

Je vous conseille alors d'utiliser le framework TestContainers-Spock pour configurer votre nombre d'instances.  
De quelles performances parle-t-on ici ? L'équipe de MockServer donne tous les détails dans leur page "Scalability & Latency".

Vous aimez voir ce qui est configuré dans MockServer ? Il fournit également une WebUI. Pour y accéder rien de plus simple, ouvrez votre navigateur et allez sur <span class="css-span">http(s)://<host>:<port>/mockserver/dashboard</span>. Vous pouvez également utiliser la méthode <span class="css-span">mockServerClient.openUI()</span> pendant votre session de debug, il ouvrira alors une nouvelle fenêtre dans votre navigateur vers sa WebUI.

L'API externe à laquelle votre application fait appel, est sécurisée via TLS ? MockServer sait gérer ça aussi.

Pour les aficionados de Kubernetes, MockerServer a un chart Helm...

Wiremock

Wiremock est très similaire à MockServer en termes de fonctionnalités. Il est également  intégrable dans Testcontainers.
Les étapes de son intégration dans les tests, en tant que conteneur Docker, sont quasiment similaires à MockServer :

  • Ajout de la dépendance `org.wiremock.integrations.testcontainers:wiremock-testcontainers-module`
  • Annotation de la classe de test avec `@Testcontainers`
  • Déclaration du conteneur "_wiremockServer_" dans la classe de test

<pre><code>
   @Testcontainers
   class WireMockIntegrationTest {
     
       @Container
       private static final WireMockContainer wiremockServer = new WireMockContainer("wiremock/wiremock")
           .withCopyFileToContainer(MountableFile.forClasspathResource("my_spec.json"), "/home/wiremock/mappings/my_spec.json");

       
       [...]
 </code></pre>

 où le fichier "my_spec.json" contient par exemple :

<pre><code>
   {
     "request": {
       "method": "GET",
       "url": "/my_endpoint"
     },
     "response": {
       "status": 200,
       "body": "I am a mock!"
     }
   }
</code></pre>

  • Enfin, il suffit de requêter notre application pour vérifier qu'elle utilise bien le mock défini et que la réponse correspond à celle attendue.

Utiliser des fichiers de mapping "Requête/Réponse" est une des solutions pour configurer WireMock.  
L'utilisation de l'API REST d'administration exposée par ce dernier en est une autre et elle peut être utilisée pour une configuration dite "à la volée" pendant l'exécution des tests. Par exemple, à l'aide du framework RestAssured :

<pre><code>
given().baseUri(wiremockServer.getHost()) // URL du mock
       .port(wiremockServer.getPort()) // port du mock
       .contentType(ContentType.JSON)
       .body("{\"request\": {\"method\": \"GET\", \"url\": \"/my_path\"}, \"response\": {\"status\": 200, \"body\": \"I am a mock!\"}}") // mapping requête/réponse
       .when()
       .post("/__admin/mappings"); // requête
</code></pre>

Notre exemple d'utilisation de WireMock est disponible ici.

Encore faim ?

Vous souhaitez configurer très finemement WireMock ? Par exemple, vous souhaitez qu'il réponde différemment s'il reçoit une requête POST dont le body contient un numéro de téléphone finissant par 400 ? C'est possible ! Il suffit d'ajouter un matcher dans la requête configurée, tel que :

<pre><code>
"bodyPatterns": [{"matchesJsonPath": "$.phoneNumber", "matches": ".*400$"}]
</code></pre>

Vous l'aurez compris, WireMock offre une configuration flexible des réponses et offre ainsi un contrôle plus fin du comportement de l'API simulée.

WireMock permet également de simuler des délais de réponse / de latence réseau, des pannes de service et du load balancing. Ces fonctionnalités permettent de simuler des scénarios plus complexes et plus réalistes.

Enfin, tout comme MockServer, WireMock permet de vérifier les requêtes reçues et leurs nombres.

OpenAPI Mock

TestContainer fournit une librairie pour MockServer et une librairie pour Wiremock. Mais il n'en fournit pas pour OpenAPI-mock. Peu importe, TestContainer permet via ses GenericContainer d'utiliser n'importe quelle image docker au sein de vos tests.

En termes de fonctionnalités, il est également bien moins fourni que les deux autres, il s'appuie essentiellement sur le contrat d'interface OpenAPI V3 et il dispose de plusieurs modes d'utilisation configurables via la variable d'environnement <span class="css-span">OPENAPI_MOCK_USE_EXAMPLES</span> :

  • <span class="css-span">no</span> : les exemples de valeurs présents dans le contrat d'interface sont ignorés et les valeurs sont générées aléatoirement par le mock. Ces valeurs se basent également sur les restrictions présentes dans le contrat d'interface (minLength, maxLength, nullable, type : string, integer...).
  • <span class="css-span">if_present</span> : les exemples de valeurs sont utilisés s'ils sont présents dans le contrat d'interface, sinon les valeurs sont générées aléatoirement.
  • <span class="css-span">exclusively</span> : les exemples de valeurs présents dans le contrat d'interface sont exclusivement utilisés, ce qui implique de les avoir renseignés pour chaque champ.

OpenAPI mock contient d'autres clés de configuration telles que par exemple le taux de probabilité de valeurs nulles générées.

<pre><code>
@Testcontainers
class OpenApiMockIntegrationTest {

 @Container    
 static final GenericContainer<?> openApiMock = new GenericContainer<>("muonsoft/openapi-mock:0.3.9")
         .withExposedPorts(8080)
         .withCopyFileToContainer(MountableFile.forHostPath("./src/test/resources/openAPI_CI.yml"), "/tmp/spec.yaml")
         .withEnv(new HashMap<>() {{
             put("OPENAPI_MOCK_SPECIFICATION_URL", "/tmp/spec.yaml");
             put("OPENAPI_MOCK_USE_EXAMPLES", "if_present");
         }});
</code></pre>

Nous avons tenu à présenter cette solution, certes beaucoup moins fournie que les deux autres, pour sa simplicité d'utilisation. Elle peut facilement être intégrée pour mocker une application exposant une API REST très simple.

Notre exemple d'utilisation de OpenAPI Mock est disponible ici.

A vous de choisir

Avoir des mocks dits "intelligents" est un vrai plus lorsque nous écrivons des tests d'intégration sur une application ayant des interactions avec des API REST externes.

MockServer et WireMock sont clairement deux solutions parmi les plus complètes du marché et elles sont utilisables et intégrables très facilement.

OpenAPI Mock peut très vite tirer son épingle du jeu dès que nous souhaitons tester des API très simples ou tester les cas nominaux de chaque endpoint d'une API. Sa fonctionnalité de génération aléatoire de valeurs dans les réponses est un vrai plus.

Documentations officielles :

No items found.
ça t’a plu ?
Partage ce contenu
Cédric

Cédric est un Architecte Logiciel, mais plutôt Backend et plutôt Java. Il aime toutes les technologies, surtout l'éco-système Spring, puisque Pivotal arrive toujours à sortir de nouvelles librairies et continue de surprendre la communauté. Mais bon, il en aime plein d'autres : l'architecture Monolithique Modulaire et le Domain Driven Design, Sonarqube et le Clean as You Code, les bases de données relationnelles... En dehors de son métier-passion, il déteste deux choses : le mois de janvier sans neige, et s'ennuyer. Les 2 sont d'ailleurs peut-être liées...