Hallo Spaß-Coder.
Bei unseren Artikeln zum Thema Testautomatisierung haben wir immer mal wieder darauf hingewiesen, dass wir keine Abhängigkeiten zur Peripherie haben möchten, um möglichst ungestört und schnell testen zu können. Wer hat sich bei diesen Artikeln nicht schon einmal die Frage gestellt, was das denn in der Praxis bedeutet?
Heute möchten wir uns an einem ganz konkreten Beispiel einmal anschauen, wie das nun funktioniert mit dem Abtrennen der Abhängigkeiten für den Test.
Devided REST-Rooms
Das Konzept der RESTful Services teilt sich in zwei Teile auf.
Auf der einen Seite haben wir den Server, der REST-Anfragen entgegen nimmt, verarbeitet und ein Ergebnis zurückliefert. Die Kommunikation erfolgt via HTTP, das Austauschformat ist frei wählbar, hier hat sich allerdings für die meisten Services JSON als Format etabliert, wenn es um den Austausch von Daten geht.
Auf der anderen Seite steht der Client, der die REST-Anfrage via HTTP stellt, das Ergebnis vom REST-Server bekommt und weiterverarbeitet.
Zwischen diesen beiden beteiligten findet eine Kommunikation statt. Der eine kann ohne den anderen nicht funktionieren, es wird immer eine Gegenstelle benötigt, um die Verarbeitung vollständig abzuschließen. Genau dies führt zu dem Problem, dass wir die beiden Teile des REST-Services nicht gut testen können.
Was aber können wir tun? Zunächst sichern wir alle Einzelteile auf der jeweiligen Seite die wir implementieren mit Unit-Tests ab. Das machen wir immer, also auf jeden Fall auch hier. Je früher wir in der Kette einen Fehler finden – und die Unit ist die Kleinste Einheit, die einen Fehler haben kann – desto leichter fällt die Analyse, die Behebung und das Sicherstellen, dass der Fehler nicht wieder auftritt.
REST in Peace – Client
Wir haben also unseren RESTful-Client geschrieben und die interne Verarbeitungen mit Unit-Tests abgesichert. Nun möchten wir aber auch die Methoden, welche die REST-Daten benötigen automatisiert testen. Wie machen wir das?
Mocking
Auf dieses Konzept sind wir in der Vergangenheit bereits eingegangen. Wir nutzen ein Mocking-Framework um die externen Abhängigkeiten zu verstecken. Wir nutzen in solchen Fällen für unseren Java-Code das Framework Mockito, ein aktives Open-Source-Projekt, welches sehr mächtig und gut dokumentiert ist.
Nehmen wir als Beispiel eine Kochbuch-Anwendung. Hierbei werden Rezepte (Recipes) und Zutaten (Ingredients) verwaltet. Wir wollen das Repository, welches die Rezepte vom Server abfragt testen, ohne dass die tatsächliche Abfrage zum Server durchgeführt wird.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
@Test public void testFindRecipesByIngredientName() throws Exception { JsonConfiguration jsonConfiguration = mock(jsonConfiguration.class) RestService restService = mock(RestService.class); Recipe[] expected = new Recipe[1]; Recipe recipe = new Recipe(); recipe.setId("38"); recipe.setName("Spaghetti Bolognese"); expected[0] = recipe; when(jsonConfiguration.getUrl()).thenReturn(""); when(restService.findObject(anyString(), anyString(), any())).thenReturn(expected); RecipeRestRepository sut = new RecipeRepository(jsonConfiguration, restService); Recipe[] actual = sut.ReadByIngredientName("Spaghetti"); Assertions.assertThat(actual[0].getId()).isEqualTo("38"); Assertions.assertThat(actual[0].getName()).isEqualTo("Spaghetti Bolognese"); } |
Wie wir sehen hilft es uns hier, dass wir mit Constructor-based Injection arbeiten. Auf diese Weise können wir dem REST-Client im Konstruktor die erstellten Mock-Objekte übergeben und diesen damit vom Server abschneiden. Wir können also in Frieden unseren Test durchführen.
Durch das Mocken der Objekte befinden wir uns weiterhin auf der Ebene des Unit-Tests, da wir die integrierten Abhängigkeiten durch Mock-Objekte ersetzt haben.
Stub-Server
Wollen wir nun aber tatsächlich mal schauen, wie sich der REST-Client denn in einer „echten Welt“ verhält, müssen wir einen Integrationstest schreiben. Wir brauchen dazu auf jeden Fall einen REST-Server, der unsere REST-Anfrage beantwortet.
1 2 3 4 5 6 7 |
[configuration.server=http://testserver:8082] @Override public Recipe[] findByIngredient(String ingredientName) { final String url = jsonConfiguration.getUrl() + "/findByIngredient/" + ingredientName; return restService.findObject(url, "Error finding recipe.", Recipe[].class); } |
Vielleicht stellt ihr euch die Frage, warum wir denn nicht gleich gegen den echten Server testen. Nun, das können wir in dem Beispiel natürlich gefahrlos tun, hier werden ja nur GET-Anfragen aufgerufen, also Daten abgefragt.
Aber steht uns der Server überhaupt zur Verfügung? Wenn wir ein Fremdes System anbinden, ist das ggf. gar nicht der Fall. Dafür müssen wir unter Umständen sehr viel Aufwand betreiben um die Infrastruktur entsprechend einzurichten.
Auf der Anderen Seite wollen wir ja auch alle Facetten des REST-Clients testen. Wie sieht es z.B. aus, wenn wir keinen GET, sondern einen PUT machen, also Daten verändern. Oder gar ein DELETE, also Daten löschen. Das möchten wir sicher nicht auf einem Produktivsystem machen.
Den Stub-Server können wir für alle Aufrufe so gestalten, dass er genau die Daten zurückgibt, die wir erwarten, ohne teure Anbindung an weitere Infrastrukturen. Auf diesem Weg können wir auch das Fehlerverhalten testen, indem wir einfach mal „Müll“ oder Fehlercodes – wie etwa keine Berechtigung oder einen internen Serverfehler – zurückgeben.
Control the REST – Server
Auf Seiten des REST-Servers haben wir eine ganz andere Herausforderung. In aller Regel haben wir eine Webanwendung mit einem Controller und einem RequestMapping. Wie aber testen wir dies? Das Mapping, den Controller, die Rückgabe, das Fehlerverhalten?
Hinweis: Dieses Verfahren funktioniert nur dann, wenn wir unseren Controller mit dem Framework Spring Web MVC implementiert haben, da die eingesetzte Mocking-Bibliothek ebenfalls aus dem Springframework stammt (Spring Test).
Auch hier testen wir zunächst die kleinsten Einheiten mit Hilfe von Unit Tests. Für die request-basierten Tests nutzen wir wiederum die Möglichkeit des Mockings. Spring Test bietet uns hier die Möglichkeit komfortabel einen Controller über einen gefakten Request aufzurufen und das Ergebnis zu prüfen.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
@Test public void testGetRecipeByIngredientRespondsWithJson() throws Exception { RecipeRepository recipeRepository = mock(RecipeRepository.class); String recipeId = "38"; String recipeName = "Spaghetti Bolognese"; Recipe recipe = new Recipe(); recipe.setId(recipeId); recipe.setName(recipeName); when(recipeRepository.findByIngredient(ingredientName)) .thenReturn(recipe); final MockMvc mockMvc = standaloneSetup(new RecipeController(recipeRepository)) .build(); mockMvc.perform(get("/recipes/findByIngredientName/" + "Spaghetti")) .andExpect(status().isOk()) .andExpect( content().contentType(MediaType.APPLICATION_JSON_VALUE)) .andExpect(jsonPath("$.recipeId").value(recipeId)) .andExpect(jsonPath("$.recipeName").value(recipeName)); } |
Wie wir sehen können wir nach dem Setup des Controllers mit Hilfe des Test-Frameworks von Spring diesen schön einfach aufrufen und somit auf Herz und Nieren Testen.
Dies ist natürlich einer der einfachsten Tests. Spannend wird es auch hier insbesondere dann, wenn wir Fehlerverhalten, falsche Parameter, etc. testen.
Zusammenfassung
Wie so oft kommt es auch beim Test von RESTful Services darauf an, die Stellen zu erkennen, die uns den Test erschweren und diese möglichst abzukapseln.
Wichtig ist noch der Hinweis, dass wir beim Test immer darauf achten müssen, nicht das wegzumocken, was wir eigentlich testen möchten. Das ist leichter gesagt, als es sich in der Realität darstellt. Ist am Anfang noch klar, was getestet werden soll, verrennt man sich ggf. schnell in Mock-Objekten, insbesondere bei Legacy-Code.
Bei neuen REST-Clients oder REST-Services sollte dieses Problem nicht so schnell auftreten, Vorsicht ist trotzdem geboten. Aber die lassen wir ja ohnehin immer walten bei unserem Code, wir sind schließlich Profis 😉
In diesem Sinne, viel Spaß beim Testen der RESTs dieser Welt 😉
Eure Spaß-Coder
Dieser Artikel basiert neben unseren Erfahrungen auf den Ausführungen aus:
- Kurzes, englischsprachiges Video zum Thema auf der Seite des Level Up Lunch