Serveur simulé pour l'automatisation des tests mobiles

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.

image

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.

Source: https://habr.com/ru/post/fr430530/


All Articles