De nos jours, l’utilisation de clients HTTP est très fréquente dans les projets. On s’en sert principalement pour communiquer avec les APIs extérieures. Dans les tests de ces projets, il n’est pas du tout recommandé d’appeler les APIs directement pour la stabilité des tests et pour éviter de polluer les serveurs avec des appels de contenu identique. C’est pour cela qu’on choisit de mocker les réponses de ces appels.
Dans cet article, je vais vous présenter une librairie de test qui répond à ce besoin.
MockWebServer
MockWebServer est conçu par OkHttp. OkHttp est un client HTTP très répandu qu’on peut retrouver dans les frameworks connus comme Spring. MockWebServer est codé en Kotlin et il est fréquemment utilisé dans les tests de projets mobiles. Il fonctionne tout aussi bien dans un environnement Java comme on peut le voir dans les tests d’intégration de Spring WebFlux.
La librairie ne propose pas de fonctionnalités avancées comme ses concurrents : WireMock et MockServer supportent le lancement en standalone et permettent de proxyer les requêtes HTTP. MockServer propose en plus une UI de debug. MockWebServer est beaucoup moins compliqué, il propose la fonctionnalité de mocker les réponses HTTP et c’est tout. Dans son GitHub, on peut constater qu’il est constitué seulement d’un serveur, d’un dispatcher et des modèles représentant les requêtes et les réponses. La librairie complète se compose d’un unique jar de seulement 72 KB. C’est très peu par rapport à ses concurrents, mais c’est tout ce qu’il nous faut pour mettre en place un fake serveur dans nos tests.
Un point à noter sur la dernière version alpha de MockWebServer, à partir de la version 5.0.0-alpha.2 MockWebServer aura un nouveau nom de package et l'ancien répertoire sera déprécié. Dans cette nouvelle version, on aura trois alternatives de librairies : avec JUnit5, avec JUnit4 ou sans dépendances JUnit.
Configuration
Pour la mise en place de l’outil, ajouter les dépendances suivantes dans votre pom.xml dans le cas de Maven (pour d’autres outils de gestion de dépendances, voir le site Maven Repository) :
<!-- MockWebServer -->
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>${mockwebserver.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>mockwebserver</artifactId>
<version>${mockwebserver.version}</version>
<scope>test</scope>
</dependency>
Si on utilise Spring Boot Starter Parent, la première dépendance okhttp est déjà incluse dedans. Dans ce cas, il faudra soit synchroniser la version avec celle apportée par Spring Boot, soit, quand on veut utiliser une version récente, écraser la version de OkHttp3 (la propriété correspondante dans Spring Boot Starter Parent est <okhttp3.version>
).
Après l’ajout de dépendances, on peut directement commencer à l’utiliser dans les tests d’intégration.
MockWebServer
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
@AutoConfigureMockMvc
@ActiveProfiles("test")
class SiteControllerIT {
private static MockWebServer mockWebServer;
@Autowired
private MockMvc mockMvc;
@BeforeAll
static void setUp() throws IOException {
mockWebServer = new MockWebServer();
mockWebServer.start(18080);
}
@AfterAll
static void tearDown() throws IOException {
mockWebServer.shutdown();
}
}
L’objet MockWebServer
est déclaré static
car on effectue son initialisation dans une méthode annotée @BeforeAll
. Cette annotation exige que la méthode soit static
.
L’initialisation est simple. On crée l’objet puis on appelle sa méthode start()
. Sans argument, MockWebServer
va démarrer sur un port aléatoire. En pratique, cela ne nous arrange pas, car on a besoin de connaître l’url à appeler. Dans la plupart des cas, on renseigne l’url du serveur appelé dans un fichier de configuration, c’est pour cela qu’on a besoin que MockWebServer
démarre sur une adresse connue. Dans l’exemple de code précédent, MockWebServer
est en écoute sur le port 18080, l’url est donc http://localhost:18080.
site.service.url: http://localhost:18080
À la fin des tests, il est nécessaire d’arrêter le MockWebServer
en utilisant la méthode shutDown()
. Comme on l’a démarré dans @BeforeAll
, il est logique de l’arrêter dans un @AfterAll
. Si la méthode shutDown()
n’est pas invoquée, MockWebServer
occupera l’adresse jusqu’à la fin du déroulement des tests et bloquera l’utilisation de cette adresse lors des autres tests d’intégration.
MockResponse
Avant de démarrer les tests, il est important de mocker les réponses pour que notre MockWebServer
réagisse face aux requêtes des clients HTTP. Il utilise MockResponse
comme conteneur de réponse HTTP. Par défaut, MockResponse
contient un corps de réponse vide et un code de retour 200. Cette réponse est totalement paramétrable. Il est possible d’ajouter des headers, modifier le corps de réponse et aussi le code de retour.
MockResponse mockResponse = new MockResponse()
.addHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON)
.setResponseCode(200)
.setBody(output);
MockWebServer
dispose de deux méthodes différentes pour stocker ces réponses.
Enqueue
La méthode enqueue()
offre une façon simple de stocker les MockResponse
. Il suffit d’ajouter la MockResponse
au MockWebServer
en appelant la méthode enqueue()
sans rien préciser d’autre. MockWebServer
répondra dans l’ordre d’insertion des MockResponse
. La méthode utilisée est donc du FIFO (First In First Out).
mockWebServer.enqueue(mockResponse);
Dans le cas où le client HTTP effectue un appel alors que la file d’attente est vide, le client HTTP va attendre la réponse de MockWebServer
. Le temps d’attente dépend du timeout configuré sur le client HTTP.
Dans le cas où l’ordre n’est pas respecté, le client HTTP recevra des réponses incorrectes et provoquera des erreurs conduisant à l’échec du test.
C’est pour cela que je recommande une deuxième solution, l’utilisation du dispatcher.
Dispatcher
La classe Dispatcher
est une classe abstraite dont nos Dispatcher
personnalisés devront hériter. Il dispose de trois méthodes et celle qui va nous intéresser est la méthode dispatch()
.
public class SiteClientDispatcher extends Dispatcher {
private final ObjectMapper mapper;
public SiteClientDispatcher() {
this.mapper = new ObjectMapper();
}
@NonNull
@Override
public MockResponse dispatch(@NonNull RecordedRequest request) {
try {
if ("/sites/data".equals(request.getPath())) {
JsonNode requestBody = mapper.readTree(request.getBody().readString(StandardCharsets.UTF_8));
JsonNode expectedInput = mapper.readTree(readFile("src/test/resources/controller/input-uuids-sites.json"));
if (requestBody.equals(expectedInput)) {
return createResponseWithJsonContent(readFile("src/test/resources/controller/output-sites-data.json"));
}
}
} catch (IOException ioe) {
return new MockResponse().setResponseCode(500);
}
return new MockResponse().setResponseCode(404);
}
private String readFile(String path) throws IOException {
return Files.readString(Paths.get(path));
}
private MockResponse createResponseWithJsonContent(String responseBody) throws IOException {
return new MockResponse()
.addHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON)
.setResponseCode(200)
.setBody(responseBody);
}
}
À l’aide de la méthode dispatch()
, on peut maintenant analyser la requête envoyée par le client HTTP et déterminer la MockResponse
la plus appropriée. Dans l’exemple, je me suis basé sur le chemin de la requête et le corps de la requête pour déterminer la MockResponse
à renvoyer (voir la Javadoc pour découvrir les autres méthodes : RecordedRequest
). La méthode dispatch()
nous oblige à fournir une réponse par défaut de type MockResponse
.
L’implémentation est plus complexe que celle de enqueue()
. En revanche, on évite les deux inconvénients décrits précédemment (sensible à l’ordre de la file d’attente et timeout en cas de file d’attente vide). Il est intéressant de noter que la méthode enqueue()
est un décorateur d’un dispatcher particulier, QueueDispatcher
.
Dans l’exemple de Dispatcher
, on peut voir que des vérifications ont été faites sur le chemin et aussi sur le corps de la requête avant de renvoyer une réponse positive. Il est aussi possible d’effectuer ces vérifications avec la méthode enqueue()
utilisant une méthode qui s’appelle takeRequest()
.
TakeRequest
RecordedRequest request = mockWebServer.takeRequest();
assertThat(request.getPath()).isEqualTo("/sites/data");
JSONAssert.assertEquals(request.getBody().readString(UTF_8), input, true);
Dans cet exemple, on peut voir que la sortie de la méthode takeRequest()
est l’objet RecordedRequest
, déjà rencontré dans le Dispatcher
. La méthode takeRequest()
fonctionne de la même manière que enqueue()
, les requêtes reçues par MockWebServer
sont stockées dans une file d’attente FIFO. À noter qu’elle a le même problème que enqueue()
lorsque la file d’attente est vide.
takeRequest()
est très utile lorsqu’on utilise la méthode enqueue()
car on ne vérifiait pas la requête d’entrée. Dans le cas de Dispatcher
, il est aussi possible de récupérer les requêtes via takeRequest()
, cependant c’est redondant avec les vérifications faites dans la méthode dispatch()
.
Conclusion
Les fonctionnalités avancées comme le lancement en standalone et le proxying sont intéressantes. Mais pour une grande partie des projets utilisant un client HTTP, seule la fonctionnalité de mocker les appels des clients HTTP dans les tests est nécessaire. MockWebServer est une librairie plus simple que ses concurrents et répond très bien à ce besoin. Elle peut être une solution plus adaptée pour ces projets de faible envergure.