En travaillant sur le dernier projet, j'ai Ă©tĂ© confrontĂ© au test d'une application mobile connectĂ©e au niveau de la logique mĂ©tier avec diffĂ©rents services tiers. Tester ces services ne faisait pas partie de ma tĂąche, cependant, des problĂšmes avec leur API ont bloquĂ© le travail de l'application elle-mĂȘme - les tests ont Ă©chouĂ© non pas Ă cause de problĂšmes Ă l'intĂ©rieur, mais Ă cause de l'inopĂ©rabilitĂ© de l'API, avant mĂȘme d'avoir vĂ©rifiĂ© la fonctionnalitĂ© nĂ©cessaire.
Traditionnellement, les supports sont utilisés pour tester de telles applications. Mais ils ne fonctionnent pas toujours normalement, ce qui interfÚre avec le travail. Comme solution alternative, j'ai utilisé moki. Je veux parler de ce chemin épineux aujourd'hui.

Afin de ne pas toucher au code d'un vrai projet (sous NDA), pour plus de clartĂ©, j'ai créé un simple client REST pour Android, qui permet d'envoyer des requĂȘtes HTTP (GET / POST) Ă une certaine adresse avec les paramĂštres dont j'ai besoin. Nous allons le tester.
Le code d'application client, les rĂ©partiteurs et les tests peuvent ĂȘtre
téléchargés depuis GitLab .
Quelles sont les options?
Il y avait deux façons de se moquer dans mon cas:
- dĂ©ployer un faux serveur dans le cloud ou sur une machine distante (si nous parlons de dĂ©veloppements confidentiels qui ne peuvent pas ĂȘtre transfĂ©rĂ©s vers le cloud);
- lancez le faux serveur localement - directement sur le téléphone sur lequel l'application mobile est testée.
La premiĂšre option n'est pas trĂšs diffĂ©rente du banc d'essai. En effet, il est possible d'allouer un poste de travail dans le rĂ©seau pour le faux serveur, mais il devra ĂȘtre pris en charge, comme tout banc d'essai. Et ici, il est nĂ©cessaire de rencontrer les principaux piĂšges de cette approche. Le poste de travail distant est mort, a cessĂ© de rĂ©pondre, quelque chose a changĂ© - vous devez surveiller, modifier la configuration, c'est-Ă -dire faire la mĂȘme chose qu'avec le support d'un banc d'essai rĂ©gulier. Nous ne pouvons pas corriger la situation par nous-mĂȘmes, et cela prendra certainement plus de temps que toute manipulation locale. Donc, spĂ©cifiquement dans mon projet, il Ă©tait plus pratique d'Ă©lever localement le faux serveur.
Choisir un faux serveur
Il existe de nombreux outils différents. J'ai essayé de travailler avec plusieurs et dans presque tout le monde, j'ai rencontré certains problÚmes:
- Mock-server , wiremock - deux faux serveurs que je ne pouvais pas exĂ©cuter normalement sur Android. Ătant donnĂ© que toutes les expĂ©riences ont eu lieu dans le cadre d'un projet en direct, le temps de choix Ă©tait limitĂ©. AprĂšs avoir creusĂ© avec eux quelques jours, j'ai renoncĂ© Ă essayer.
- Restmock est un wrapper sur okhttpmockwebserver , qui sera discutĂ© plus en dĂ©tail plus tard. Ăa avait l'air bien, ça a commencĂ©, mais le dĂ©veloppeur de ce wrapper a cachĂ© «sous le capot» la possibilitĂ© de dĂ©finir l'adresse IP et le port du faux serveur, et pour moi, c'Ă©tait critique. Restmock a commencĂ© sur un port alĂ©atoire. En fouillant dans le code, j'ai vu que lorsque le serveur a Ă©tĂ© initialisĂ©, le dĂ©veloppeur a utilisĂ© une mĂ©thode qui dĂ©finissait le port au hasard s'il ne le recevait pas Ă l'entrĂ©e. En principe, on pouvait hĂ©riter de cette mĂ©thode, mais le problĂšme Ă©tait dans le constructeur privĂ©. En consĂ©quence, j'ai refusĂ© l'emballage.
- Okhttpmockwebserver - aprĂšs avoir essayĂ© diffĂ©rents outils, je me suis arrĂȘtĂ© sur le faux serveur, qui se rĂ©unissait normalement et a commencĂ© localement sur l'appareil.
Nous analysons le principe du travail
La version actuelle de okhttpmockwebserver vous permet d'implémenter plusieurs scénarios de travail:
- File d'attente de réponses . Les réponses du faux serveur sont ajoutées à la file d'attente FIFO. Peu importe quelle API et quel chemin j'accéderai, le faux serveur lancera à tour de rÎle des messages dans cette file d'attente.
- Le répartiteur vous permet de créer des rÚgles qui déterminent la réponse à donner. Supposons qu'une demande provienne d'une URL contenant un chemin, par exemple / get-login /. Sur ce serveur / get-login / mock et donne une réponse unique et prédéfinie.
- VĂ©rificateur de demande . Sur la base du scĂ©nario prĂ©cĂ©dent, je peux vĂ©rifier les requĂȘtes que l'application envoie (que dans les conditions donnĂ©es, une requĂȘte avec certains paramĂštres part vraiment). Cependant, la rĂ©ponse est sans importance, car elle est dĂ©terminĂ©e par le fonctionnement de l'API. Ce script implĂ©mente le vĂ©rificateur de demandes.
Examinez chacun des scénarios plus en détail.
File d'attente de réponse
L'implĂ©mentation la plus simple du faux serveur est la file d'attente de rĂ©ponses. Avant le test, je dĂ©termine l'adresse et le port oĂč le faux serveur sera dĂ©ployĂ©, ainsi que le fait qu'il fonctionnera sur le principe d'une file d'attente de messages - FIFO (premier entrĂ©, premier sorti).
Ensuite, exécutez le faux serveur.
class QueueTest: BaseTest() { @Rule @JvmField var mActivityRule: ActivityTestRule<MainActivity> = ActivityTestRule(MainActivity::class.java) @Before fun initMockServer() { val mockServer = MockWebServer() val ip = InetAddress.getByName("127.0.0.1") val port = 8080 mockServer.enqueue(MockResponse().setBody("1st message")) mockServer.enqueue(MockResponse().setBody("2nd message")) mockServer.enqueue(MockResponse().setBody("3rd message")) mockServer.start(ip, port) } @Test fun queueTest() { sendGetRequest("http://localhost:8080/getMessage") assertResponseMessage("1st message") returnFromResponseActivity() sendPostRequest("http://localhost:8080/getMessage") assertResponseMessage("2nd message") returnFromResponseActivity() sendGetRequest("http://localhost:8080/getMessage") assertResponseMessage("3rd message") returnFromResponseActivity() } }
Les tests sont écrits à l'aide du framework Espresso, conçu pour effectuer des actions dans les applications mobiles. Dans cet exemple, je sélectionne les types de demande et les envoie à tour de rÎle.
AprÚs avoir commencé le test, le faux serveur lui donne des réponses conformément à la file d'attente prescrite et le test réussit sans erreur.
Implémentation de Dispatcher
Un répartiteur est un ensemble de rÚgles selon lesquelles un faux serveur fonctionne. Pour plus de commodité, j'ai créé trois répartiteurs différents: SimpleDispatcher, OtherParamsDispatcher et ListingDispatcher.
Simpledispatcher
Okhttpmockwebserver fournit la classe
Dispatcher()
pour implémenter le répartiteur. Vous pouvez en hériter en remplaçant la fonction de
dispatch
Ă votre maniĂšre.
class SimpleDispatcher: Dispatcher() { @Override override fun dispatch(request: RecordedRequest): MockResponse { if (request.method == "GET"){ return MockResponse().setResponseCode(200).setBody("""{ "message": "It was a GET request" }""") } else if (request.method == "POST") { return MockResponse().setResponseCode(200).setBody("""{ "message": "It was a POST request" }""") } return MockResponse().setResponseCode(200) } }
La logique de cet exemple est simple: si un GET arrive, je renvoie un message qu'il s'agit d'une demande GET. Si POST, je renvoie un message sur la demande POST. Dans d'autres situations, je renvoie une demande vide.
SimpleDispatcher
apparaĂźt dans le test - un objet de la classe
SimpleDispatcher
, que j'ai dĂ©crit ci-dessus. De plus, comme dans l'exemple prĂ©cĂ©dent, le serveur simulĂ© est lancĂ©, mais cette fois, une sorte de rĂšgle pour travailler avec ce serveur simulĂ© est indiquĂ©e - le mĂȘme rĂ©partiteur.
Les sources de test avec
SimpleDispatcher
peuvent ĂȘtre trouvĂ©es
dans le référentiel .
AutreParamsDispatcher
En remplaçant la fonction de
dispatch
, je peux repousser d'autres paramÚtres de demande pour envoyer des réponses:
class OtherParamsDispatcher: Dispatcher() { @Override override fun dispatch(request: RecordedRequest): MockResponse { return when { request.path.contains("?queryKey=value") -> MockResponse().setResponseCode(200).setBody("""{ "message": "It was a GET request with query parameter queryKey equals value" }""") request.body.toString().contains("\"bodyKey\":\"value\"") -> MockResponse().setResponseCode(200).setBody("""{ "message": "It was a POST request with body parameter bodyKey equals value" }""") request.headers.toString().contains("header: value") -> MockResponse().setResponseCode(200).setBody("""{ "message": "It was some request with header equals value" }""") else -> MockResponse().setResponseCode(200).setBody("""{ Wrong response }""") } } }
Dans ce cas, je démontre plusieurs options pour les conditions.
Tout d'abord, vous pouvez transmettre des paramÚtres à l'API dans la barre d'adresse. Par conséquent, je peux mettre une condition sur l'entrée de n'importe quel bundle dans le chemin, par exemple,
â?queryKey=valueâ
.
DeuxiĂšmement, cette classe vous permet d'entrer dans le corps du corps des requĂȘtes POST ou PUT. Par exemple, vous pouvez utiliser
contains
en exécutant d'abord
toString()
. Dans mon exemple, la condition est dĂ©clenchĂ©e lorsqu'une requĂȘte POST contenant
âbodyKeyâ:âvalueâ
. De mĂȘme, je peux valider l'en-
header : value
la demande (en-
header : value
).
Pour des exemples de tests, je recommande de se référer
au référentiel .
ListingDispatcher
Si nĂ©cessaire, vous pouvez implĂ©menter une logique plus complexe - ListingDispatcher. De la mĂȘme maniĂšre, je remplace la fonction de
dispatch
. Cependant, maintenant dans la classe, j'ai défini l'ensemble par défaut de
stubsList
(
stubsList
) - mok pour différentes occasions.
class ListingDispatcher: Dispatcher() { private var stubsList: ArrayList<RequestClass> = defaultRequests() @Override override fun dispatch(request: RecordedRequest): MockResponse = try { stubsList.first { it.matcher(request.path, request.body.toString()) }.response() } catch (e: NoSuchElementException) { Log.e("Unexisting request path =", request.path) MockResponse().setResponseCode(404) } private fun defaultRequests(): ArrayList<RequestClass> { val allStubs = ArrayList<RequestClass>() allStubs.add(RequestClass("/get", "queryParam=value", "", """{ "message" : "Request url starts with /get url and contains queryParam=value" }""")) allStubs.add(RequestClass("/post", "queryParam=value", "", """{ "message" : "Request url starts with /post url and contains queryParam=value" }""")) allStubs.add(RequestClass("/post", "", "\"bodyParam\":\"value\"", """{ "message" : "Request url starts with /post url and body contains bodyParam:value" }""")) return allStubs } fun replaceMockStub(stub: RequestClass) { val valuesToRemove = ArrayList<RequestClass>() stubsList.forEach { if (it.path.contains(stub.path)&&it.query.contains(stub.query)&&it.body.contains(stub.body)) valuesToRemove.add(it) } stubsList.removeAll(valuesToRemove) stubsList.add(stub) } fun addMockStub(stub: RequestClass) { stubsList.add(stub) } }
Pour ce faire, j'ai créé une classe ouverte
RequestClass
, dont tous les champs sont vides par défaut. Pour cette classe, je définis une fonction de
response
qui crée un objet
MockResponse
(renvoyant une réponse 200 ou un autre
responseText
), et une fonction de correspondance qui retourne
true
ou
false
.
open class RequestClass(val path:String = "", val query: String = "", val body:String = "", val responseText: String = "") { open fun response(code: Int = 200): MockResponse = MockResponse() .setResponseCode(code) .setBody(responseText) open fun matcher(apiCall: String, apiBody: String): Boolean = apiCall.startsWith(path)&&apiCall.contains(query)&&apiBody.contains(body) }
Par conséquent, je peux créer des combinaisons de conditions plus complexes pour les talons. Cette conception m'a semblé plus flexible, bien que le principe de base soit trÚs simple.
Mais surtout dans cette approche, j'ai aimé pouvoir remplacer certains stubs sur le pouce, s'il est nécessaire de changer quelque chose dans la réponse du serveur simulé lors d'un test. Lorsque vous testez de grands projets, ce problÚme se produit assez souvent, par exemple, lors de la vérification de certains scénarios spécifiques.
Le remplacement peut ĂȘtre effectuĂ© comme suit:
fun replaceMockStub(stub: RequestClass) { val valuesToRemove = ArrayList<RequestClass>() stubsList.forEach { if (it.path.contains(stub.path)&&it.query.contains(stub.query)&&it.body.contains(stub.body)) valuesToRemove.add(it) } stubsList.removeAll(valuesToRemove) stubsList.add(stub) }
Avec cette implémentation du répartiteur, les tests restent simples. Je démarre également le faux serveur, sélectionnez uniquement
ListingDispatcher
.
class ListingDispatcherTest: BaseTest() { @Rule @JvmField var mActivityRule: ActivityTestRule<MainActivity> = ActivityTestRule(MainActivity::class.java) private val dispatcher = ListingDispatcher() @Before fun initMockServer() { val mockServer = MockWebServer() val ip = InetAddress.getByName("127.0.0.1") val port = 8080 mockServer.setDispatcher(dispatcher) mockServer.start(ip, port) } . . . }
Par souci d'expérience, j'ai remplacé le talon par POST:
@Test fun postReplacedStubTest() { val params: HashMap<String, String> = hashMapOf("bodyParam" to "value") replacePostStub() sendPostRequest("http://localhost:8080/post", params = params) assertResponseMessage("""{ "message" : "Post request stub has been replaced" }""") }
Pour ce faire, a appelé la fonction
replacePostStub
partir d'un
dispatcher
régulier et a ajouté une nouvelle
response
.
private fun replacePostStub() { dispatcher.replaceMockStub(RequestClass("/post", "", "\"bodyParam\":\"value\"", """{ "message" : "Post request stub has been replaced" }""")) }
Dans le test ci-dessus, je vérifie que le talon a été remplacé.
Ensuite, j'ai ajouté un nouveau stub, qui n'était pas par défaut.
@Test fun getNewStubTest() { addSomeStub() sendGetRequest("http://localhost:8080/some_specific_url") assertResponseMessage("""{ "message" : "U have got specific message" }""") }
private fun addSomeStub() { dispatcher.addMockStub(RequestClass("/some_specific_url", "", "", """{ "message" : "U have got specific message" }""")) }
Vérificateur de demande
Le dernier cas - Request verifier - ne prévoit pas l'espionnage, mais vérifie les demandes envoyées par l'application. Pour ce faire, je démarre simplement le faux serveur en implémentant le répartiteur afin que l'application renvoie au moins quelque chose.
Lors de l'envoi d'une demande à partir d'un test, elle arrive au serveur factice. Grùce à lui, je peux accéder aux paramÚtres de la demande à l'aide de
takeRequest()
.
@Test fun requestVerifierTest() { val params: HashMap<String, String> = hashMapOf("bodyKey" to "value") val headers: HashMap<String, String> = hashMapOf("header" to "value") sendPostRequest("http://localhost:8080/post", headers = headers, params = params) val request = mockServer.takeRequest() assertEquals("POST", request.method) assertEquals("value", request.getHeader("header")) assertTrue(request.body.toString().contains("\"bodyKey\":\"value\"")) assertTrue(request.path.startsWith("/post")) }
Ci-dessus, j'ai montrĂ© le test avec un exemple simple. Exactement la mĂȘme approche peut ĂȘtre utilisĂ©e pour JSON complexe, y compris pour vĂ©rifier la structure entiĂšre de la demande (vous pouvez comparer au niveau JSON ou analyser JSON en objets et vĂ©rifier l'Ă©galitĂ© au niveau objet).
Résumé
En général, j'ai aimé l'outil (okhttpmockwebserver), et je l'utilise sur un grand projet. Bien sûr, il y a quelques petites choses que j'aimerais changer.
Par exemple, je n'aime pas que vous ayez Ă frapper l'adresse locale (localhost: 8080 dans notre exemple) dans les configs de votre application; peut-ĂȘtre que je peux toujours trouver un moyen de tout configurer pour que le faux serveur rĂ©ponde lorsqu'il essaie d'envoyer une demande Ă n'importe quelle adresse.
De plus, je n'ai pas la possibilitĂ© de rediriger les demandes - lorsque le faux serveur envoie une demande supplĂ©mentaire, s'il n'a pas de talon appropriĂ©. Il n'y a pas une telle approche dans ce faux serveur. Cependant, il nâa mĂȘme pas Ă©tĂ© mis en Ćuvre, car pour le moment le projet «combat» nâa pas une telle tĂąche.
Auteur de l'article: Ruslan Abdulin
PS Nous publions nos articles sur plusieurs sites du Runet. Abonnez-vous Ă nos pages sur les
chaĂźnes VK ,
FB ou
Telegram pour découvrir toutes nos publications et autres actualités Maxilect.