Article écrit par Benoit Delesalle
Il arrive parfois que les applications que nous développons soient dépendantes d’autres applications ou API.
En effet, il n’est pas impossible que mon application ait besoin de communiquer avec des services externes, tel qu’un prestataire de paiement ou une solution d’envoi de mail externalisée par exemple. Lorsque vous développez votre application, vous vous dites que c’est quand même bien de ne pas avoir à réinventer la roue et de pouvoir se baser sur une solution robuste qui a déjà fait ses preuves. Mais lors de l’écriture des tests, vous pouvez rapidement vous retrouver dans cette situation si vous n’avez pas les bons réflexes.
Wiremock, l’outil indispensable qui fait l’appli et le beau temps
Heureusement pour vous, je connais un outil qui va grandement vous faciliter l’écriture de vos tests lorsque vous avez ce genre de dépendance, et comme vous l’aurez certainement déjà compris, cet outil c’est Wiremock.
Pour notre exemple, admettons que notre application soit écrite en PHP et qu’elle affiche tout simplement la météo actuelle dans votre ville. On pourrait écrire un service qui se charge de récupérer les informations et de les formater qui ressemblerait à ceci :
<?php # src/Service/WeatherService.php namespace AppService; use GuzzleHttpClient; use SymfonyComponentDependencyInjectionParameterBagParameterBagInterface; use SymfonyComponentHttpFoundationRequest; class WeatherService { /** * @var Client */ private $client; /** * @var string */ private $baseUrl; /** * @var ParameterBagInterface */ private $apiKey; /** * WeatherService constructor. * * @param ParameterBagInterface $parameters */ public function __construct(ParameterBagInterface $parameters) { $this->client = new Client(); $this->apiKey = $parameters->get('weather_api_key'); $this->baseUrl = $parameters->get('weather_api_base_url'); } /** * @param string $city * @return array */ public function call(string $city = "Lambersart"): array { return $this->format( $this->getWeather($city) ); } /** * @param string $city * @return array */ private function getWeather(string $city): array { return json_decode( $this->client->request( Request::METHOD_GET, "$this->baseUrl?units=metric&lang=fr&q=$city&appid=$this->apiKey" ) ->getBody() ->getContents(), true ); } /** * @param array $data * @return array */ private function format(array $data): array { return [ 'temperature' => [ 'current' => $data['main']['temp'], 'feels_like' => $data['main']['feels_like'] ], 'description' => array_shift($data['weather'])['description'] ]; } }
On est d’accord qu’on pourrait améliorer le code, mais on a déjà une bonne base pour écrire notre test et qu’il pourrait ressembler à ceci :
<?php # tests/WeatherServiceTest.php namespace AppTests; use AppServiceWeatherService; use SymfonyBundleFrameworkBundleTestWebTestCase; class WeatherServiceTest extends WebTestCase { public function testCall() { self::bootKernel(); $weatherService = self::$container->get(WeatherService::class); $this->assertEquals( [ 'temperature' => [ 'current' => '7.87', 'feels_like' => '0.48' ], 'description' => 'partiellement nuageux' ] , $weatherService->call('Lille') ); } }
Si vous vous dites que quelque chose ne va pas dans ce test, alors vous avez tout bon. Effectivement, mon test est dépendant d’une API tierce et on ne peut garantir qu’à un instant T, elle renverra la même chose qu’à l’instant d’avant et on devrait mocker le résultat de cet appel car dans le fond ce qui nous intéresse réellement c’est de savoir si notre service fait bien ce qu’il est censé faire. Ainsi, l’objet de notre test est de s’assurer que le bon appel est fait à l’API, non pas de vérifier le retour de cet appel.
Pour cela, rien de plus simple avec Wiremock. En effet, il suffit d’avoir un serveur Wiremock qui tourne et qui va servir de proxy
vers l’API de météo. Ici, nous allons le lancer dans un container docker
pour plus de facilité.
docker run -d -p 81:8080 ekino/wiremock --proxy-all="http://api.openweathermap.org/data/2.5/weather" --record-mappings
Ici, un serveur Wiremock est lancé et disponible sur http://localhost:81
. Si on accède à l’administration via http://localhost:81/__admin/
, on peut voir qu’actuellement nous n’avons aucun mapping, à comprendre aucun mock enregistré. Dans notre environnement de test, la base_url
de l’API météo doit désormais pointer sur notre serveur Wiremock http://localhost:81/
.
Voici un exemple de configuration pour Symfony.
# .env WEATHER_API_BASE_URL=http://api.openweathermap.org/data/2.5/weather
# .env.test WEATHER_API_BASE_URL=http://localhost:81
Une fois ceci fait, enregistrons notre premier mock. Pour ce faire, rendons-nous sur http://localhost:81/__admin/recorder/
, renseignons l’URL de notre API de météo, à savoir http://api.openweathermap.org/data/2.5/weather
, cliquons sur Record
, relançons notre test, et enfin cliquons sur Stop
. Nous venons d’enregistrer notre premier mock qui est désormais visible sur la page d’administration.
{ "mappings":[ { "id":"48a59cf8-c8ec-45d4-8b00-f8d3952d4cfc", "name":"", "request":{ "url":"/?units=metric&lang=fr&q=Lambersart&appid=088aa94d9b354402acf23fcaba0ac4f6", "method":"GET" }, "response":{ "status":200, "body":"{""coord"":{""lon"":3.03,""lat"":50.65},""weather"":[{""id"":800,""main"":""Clear"",""description"":""ciel dégagé"",""icon"":""01d""}],""base"":""stations"",""main"":{""temp"":16.3,""feels_like"":10.34,""temp_min"":14.44,""temp_max"":17.78,""pressure"":1026,""humidity"":39},""visibility"":10000,""wind"":{""speed"":6.2,""deg"":250},""clouds"":{""all"":0},""dt"":1584543344,""sys"":{""type"":1,""id"":6559,""country"":""FR"",""sunrise"":1584510863,""sunset"":1584554240},""timezone"":3600,""id"":3008218,""name"":""Lambersart"",""cod"":200}", "headers":{ "Server":"openresty", "Date":"Wed, 18 Mar 2020 15:00:18 GMT", "Content-Type":"application/json; charset=utf-8", "Connection":"keep-alive", "X-Cache-Key":"/data/2.5/weather/?lang=fr&q=lambersart&units=metric", "Access-Control-Allow-Origin":"*", "Access-Control-Allow-Credentials":"true", "Access-Control-Allow-Methods":"GET, POST" } }, "uuid":"48a59cf8-c8ec-45d4-8b00-f8d3952d4cfc", "persistent":true } ], "meta":{ "total":1 } }
Adaptons notre test avec les valeurs retournées dans ce mock dans la partie response
.
Voilà, à partir de maintenant, en environnement de test, notre application n’appellera plus l’API météo mais prendra comme acquis les valeurs de notre mock pour cet appel HTTP.
Ainsi se termine notre article concernant Wiremock et avouez-le, vous êtes bien content de ne plus devoir écrire vos mocks à la main.