Mock-Server für die mobile Testautomatisierung

Während ich an dem neuesten Projekt arbeitete, musste ich eine mobile Anwendung testen, die auf der Ebene der Geschäftslogik mit verschiedenen Diensten von Drittanbietern verbunden war. Das Testen dieser Dienste war nicht Teil meiner Aufgabe, jedoch blockierten Probleme mit ihrer API die Arbeit der Anwendung selbst - die Tests fielen nicht aufgrund von Problemen im Inneren, sondern aufgrund der Inoperabilität der API, noch bevor die Prüfung auf die erforderliche Funktionalität abgeschlossen wurde.

Traditionell werden Ständer verwendet, um solche Anwendungen zu testen. Sie funktionieren jedoch nicht immer normal, was die Arbeit beeinträchtigt. Als alternative Lösung habe ich Moki verwendet. Ich möchte heute über diesen dornigen Weg sprechen.

Bild

Um den Code eines realen Projekts (unter NDA) nicht zu berühren, habe ich zur Klarheit der weiteren Diskussion einen einfachen REST-Client für Android erstellt, mit dem HTTP-Anforderungen (GET / POST) mit den von mir benötigten Parametern an eine bestimmte Adresse gesendet werden können. Wir werden es testen.
Client-Anwendungscode, Dispatcher und Tests können von GitLab heruntergeladen werden .

Welche Möglichkeiten gibt es?


In meinem Fall gab es zwei Ansätze zum Rauchen:

  • Bereitstellen eines Mock-Servers in der Cloud oder auf einem Remotecomputer (wenn es sich um vertrauliche Entwicklungen handelt, die nicht in die Cloud übertragen werden können);
  • Starten Sie den Mock-Server lokal - direkt auf dem Telefon, auf dem die mobile Anwendung getestet wird.

Die erste Option unterscheidet sich nicht wesentlich vom Prüfstand. Es ist zwar möglich, eine Workstation im Netzwerk für den Mock-Server zuzuweisen, diese muss jedoch wie jeder Teststand unterstützt werden. Und hier ist es notwendig, auf die Hauptfallen dieses Ansatzes zu stoßen. Die Remote-Workstation ist gestorben, reagiert nicht mehr, etwas hat sich geändert - Sie müssen überwachen, die Konfiguration ändern, d. H. Machen Sie es genauso wie mit der Unterstützung eines normalen Prüfstands. Wir können die Situation nicht für uns selbst korrigieren, und es wird sicherlich mehr Zeit in Anspruch nehmen als lokale Manipulationen. Speziell in meinem Projekt war es daher bequemer, den Mock-Server lokal zu erhöhen.

Auswählen eines Mock-Servers


Es gibt viele verschiedene Werkzeuge. Ich habe versucht, mit mehreren zu arbeiten, und bei fast allen bin ich auf bestimmte Probleme gestoßen:

  • Mock-Server , Wiremock - zwei Mock-Server, die ich unter Android nicht normal ausführen konnte. Da alle Experimente im Rahmen eines Live-Projekts stattfanden, war die Zeit für die Auswahl begrenzt. Nachdem ich ein paar Tage mit ihnen gegraben hatte, gab ich es auf, es zu versuchen.
  • Restmock ist ein Wrapper über okhttpmockwebserver , auf den später noch näher eingegangen wird. Es sah gut aus, es begann, aber der Entwickler dieses Wrappers versteckte "unter der Haube" die Möglichkeit, die IP-Adresse und den Port des Mock-Servers festzulegen, und für mich war es entscheidend. Restmock wurde an einem zufälligen Port gestartet. Beim Stöbern im Code stellte ich fest, dass der Entwickler bei der Initialisierung des Servers eine Methode verwendete, mit der der Port zufällig festgelegt wurde, wenn er bei der Eingabe nicht empfangen wurde. Im Prinzip könnte man von dieser Methode erben, aber das Problem lag im privaten Konstruktor. Infolgedessen lehnte ich die Verpackung ab.
  • Okhttpmockwebserver - Nachdem ich verschiedene Tools ausprobiert hatte, hielt ich am Mock-Server an, der normalerweise zusammenkam und lokal auf dem Gerät gestartet wurde.

Wir analysieren das Prinzip der Arbeit


Mit der aktuellen Version von okhttpmockwebserver können Sie verschiedene Arbeitsszenarien implementieren:

  • Warteschlange der Antworten . Mock-Server-Antworten werden der FIFO-Warteschlange hinzugefügt. Es spielt keine Rolle, auf welche API und auf welchen Pfad ich zugreifen werde, der Mock-Server wirft abwechselnd Nachrichten in diese Warteschlange.
  • Mit dem Dispatcher können Sie Regeln erstellen, die bestimmen, welche Antwort gegeben werden soll. Angenommen, eine Anfrage stammt von einer URL, die einen Pfad enthält, z. B. / get-login /. Auf diesem / get-login / mock-Server und gibt eine einzelne, vordefinierte Antwort.
  • Anforderungsverifizierer . Basierend auf dem vorherigen Szenario kann ich die Anforderungen überprüfen, die die Anwendung sendet (dass unter den gegebenen Bedingungen eine Anforderung mit bestimmten Parametern wirklich abläuft). Die Antwort ist jedoch unwichtig, da sie von der Funktionsweise der API abhängt. Dieses Skript implementiert die Anforderungsverifizierung.

Betrachten Sie jedes der Szenarien genauer.

Antwortwarteschlange


Die einfachste Implementierung des Mock-Servers ist die Antwortwarteschlange. Vor dem Test bestimme ich die Adresse und den Port, an dem der Mock-Server bereitgestellt werden soll, sowie die Tatsache, dass er nach dem Prinzip einer Nachrichtenwarteschlange - FIFO (first in first out) - funktioniert.

Führen Sie als Nächstes den Mock-Server aus.

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() } } 

Tests werden mit dem Espresso-Framework geschrieben, mit dem Aktionen in mobilen Anwendungen ausgeführt werden können. In diesem Beispiel wähle ich Anforderungstypen aus und sende sie nacheinander.
Nach dem Starten des Tests gibt der Mock-Server Antworten gemäß der vorgeschriebenen Warteschlange, und der Test besteht fehlerfrei.

Dispatcher-Implementierung


Ein Dispatcher ist eine Reihe von Regeln, nach denen ein Mock-Server arbeitet. Der Einfachheit halber habe ich drei verschiedene Dispatcher erstellt: SimpleDispatcher, OtherParamsDispatcher und ListingDispatcher.

Simpledispatcher


Okhttpmockwebserver stellt die Dispatcher() -Klasse zur Implementierung des Dispatchers bereit. Sie können davon erben, indem Sie die dispatch auf Ihre eigene Weise überschreiben.

 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) } } 

Die Logik in diesem Beispiel ist einfach: Wenn ein GET eintrifft, gebe ich eine Nachricht zurück, dass dies eine GET-Anforderung ist. Bei POST erhalte ich eine POST-Anforderungsnachricht. In anderen Situationen gebe ich eine leere Anfrage zurück.

SimpleDispatcher erscheint im Test - ein Objekt der SimpleDispatcher Klasse, die ich oben beschrieben habe. Wie im vorherigen Beispiel wird der Mock-Server gestartet. Nur dieses Mal wird nur eine Art Regel für die Arbeit mit diesem Mock-Server angegeben - derselbe Dispatcher.

SimpleDispatcher mit SimpleDispatcher finden Sie im Repository .

OtherParamsDispatcher


Durch Überschreiben der dispatch kann ich mich von anderen Anforderungsparametern entfernen, um Antworten zu senden:

 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 }""") } } } 

In diesem Fall zeige ich verschiedene Optionen für die Bedingungen.

Zunächst können Sie Parameter in der Adressleiste an die API übergeben. Daher kann ich eine Bedingung für den Eintrag eines beliebigen Bundles im Pfad “?queryKey=value” , z. B. “?queryKey=value” .
Zweitens können Sie mit dieser Klasse in den Hauptteil der POST- oder PUT-Anforderungen eindringen. Sie können beispielsweise toString() verwenden, indem Sie zuerst toString() ausführen. In meinem Beispiel wird die Bedingung ausgelöst, wenn eine POST-Anforderung mit “bodyKey”:”value” . Ebenso kann ich den Anforderungsheader ( header : value ) validieren.

Für Testbeispiele empfehle ich, auf das Repository zu verweisen.

ListingDispatcher


Bei Bedarf können Sie eine komplexere Logik implementieren - ListingDispatcher. Ebenso überschreibe ich die dispatch . Jetzt habe ich jedoch direkt in der Klasse den Standardsatz von stubsList ( stubsList ) - mok für verschiedene Gelegenheiten festgelegt.

 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) } } 

Zu diesem RequestClass ich eine offene Klasse RequestClass , deren Felder standardmäßig leer sind. Für diese Klasse definiere ich eine response , die ein MockResponse Objekt erstellt (eine 200-Antwort oder einen anderen responseText zurückgibt), und eine matcher Funktion, die true oder false zurückgibt.

 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) } 

Infolgedessen kann ich komplexere Kombinationen von Bedingungen für Stubs erstellen. Dieses Design erschien mir flexibler, obwohl das Prinzip im Kern sehr einfach ist.

Vor allem aber hat mir bei diesem Ansatz gefallen, dass ich einige Stubs direkt unterwegs ersetzen kann, wenn bei einem Test etwas an der Antwort des Mock-Servers geändert werden muss. Beim Testen großer Projekte tritt dieses Problem häufig auf, beispielsweise beim Überprüfen bestimmter Szenarien.
Der Austausch kann wie folgt erfolgen:

 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) } 

Mit dieser Implementierung des Dispatchers bleiben die Tests einfach. Ich ListingDispatcher auch den Mock-Server, wähle nur 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) } . . . } 

Aus Versuchsgründen habe ich den Stub durch POST ersetzt:

 @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" }""") } 

replacePostStub Funktion replacePostStub von einem regulären dispatcher und replacePostStub eine neue response .

 private fun replacePostStub() { dispatcher.replaceMockStub(RequestClass("/post", "", "\"bodyParam\":\"value\"", """{ "message" : "Post request stub has been replaced" }""")) } 

Im obigen Test überprüfe ich, ob der Stub ersetzt wurde.
Dann habe ich einen neuen Stub hinzugefügt, der nicht in der Standardeinstellung war.

 @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" }""")) } 

Anforderungsverifizierer


Der letzte Fall - Request Verifier - sieht kein Snooping vor, sondern prüft auf von der Anwendung gesendete Anfragen. Dazu starte ich den Mock-Server einfach, indem ich den Dispatcher so implementiere, dass die Anwendung zumindest etwas zurückgibt.
Wenn Sie eine Anfrage von einem Test senden, wird diese an den Mock-Server gesendet. Dadurch kann ich mit takeRequest() auf die Anforderungsparameter 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")) } 

Oben habe ich den Test anhand eines einfachen Beispiels gezeigt. Genau der gleiche Ansatz kann für komplexes JSON verwendet werden, einschließlich zum Überprüfen der gesamten Struktur der Anforderung (Sie können auf JSON-Ebene vergleichen oder JSON in Objekte analysieren und die Gleichheit auf Objektebene überprüfen).

Zusammenfassung


Im Allgemeinen hat mir das Tool (okhttpmockwebserver) gefallen und ich verwende es für ein großes Projekt. Natürlich gibt es einige kleine Dinge, die ich ändern möchte.
Zum Beispiel gefällt mir nicht, dass Sie in den Konfigurationen Ihrer Anwendung auf die lokale Adresse (in unserem Beispiel localhost: 8080) klopfen müssen. Vielleicht kann ich immer noch einen Weg finden, alles so zu konfigurieren, dass der Mock-Server reagiert, wenn er versucht, eine Anfrage an eine beliebige Adresse zu senden.
Außerdem kann ich Anforderungen nicht umleiten - wenn der Mock-Server eine Anforderung weiter sendet, wenn er keinen geeigneten Stub dafür hat. In diesem Mock-Server gibt es keinen solchen Ansatz. Es kam jedoch noch nicht einmal zu ihrer Umsetzung, da das "Kampf" -Projekt derzeit keine solche Aufgabe hat.

Artikelautor: Ruslan Abdulin

PS Wir veröffentlichen unsere Artikel auf mehreren Websites der Runet. Abonnieren Sie unsere Seiten auf VK , FB oder Telegramm-Kanal , um mehr über unsere Veröffentlichungen und andere Maxilect-Nachrichten zu erfahren.

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


All Articles