API Testing mit Java und WireMock
Motivation
Ein bekanntes Problem: Öffentliche APIs werden abgefragt und liefern Daten in Form von JSON oder anderen Formaten zurück. Diese Daten können sich je nach Abfrageort, Zeit oder Umstände ändern. Ein Programm nimmt diese Daten entgegen und verarbeitet sie weiter. Doch wie lässt sich ein automatisierter Test gestalten, wenn sich die Eingangsdaten ständig ändern können? Wie kann ein Algorithmus zuverlässig und automatisiert getestet werden?
Um dieses Problem zu lösen, wird das Konzept des Mockens eingesetzt. Dabei wird nicht mit der original API gearbeitet, sondern ein so genannter Mock, also ein simuliertes Objekt, verwendet. Dieser Blogeintrag zeigt das Konzept mit WireMock und verwendet als Demo die Open Weather Map API.
Benötigte Komponenten
- OpenWeather API Key
- WireMock
- Java
WireMock einbinden
Über Maven lässt sich WireMock sehr komfortabel einbinden, einfach nachfolgende Dependencies im Projekt ergänzen.
<dependency>
<groupId>com.github.tomakehurst</groupId>
<artifactId>wiremock</artifactId>
<version>1.58</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.12.4</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>2.12.4</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
<version>2.12.4</version>
</dependency>
WireMock und Junit
WireMock simuliert eine HTTP (oder HTTPS) basierte API. Dies ermöglicht es auf APIs zuzugreifen die nicht immer verfügbar sind oder bei der ein Zugriff teuer sein kann. Sogar wenn die API noch nicht fertig entwickelt ist, lassen sich Tests schreiben und somit wertvolle Zeit einsparen. WireMock startet einen Server, lädt dort das vorgegebene API-Abfrageergebnis und ermöglicht einen Zugriff darauf.
WireMock arbeitet hervorragend mit JUnit zusammen. Über die Rule-Annotation lässt sich der Lifecycle des Servers kompakt und einfach steuern. Kein Server Starten und Beenden ist mehr durch den Entwickler nötig.
@Rule
public WireMockRule wireMockRule = new WireMockRule(wireMockConfig().dynamicPort());
Durch die Rule-Annotation kümmert sich JUnit ganz automatisch um den WireMock Server. Über die wireMockConfig lassen sich noch verschiedene Konfigurationen mitgeben. In diesem Beispiel wird immer ein zufälliger, freier Port für den Server verwendet.
Erstes Beispiel
Nachfolgend wird der erste Mock erstellt.
wireMockRule.stubFor(get(WireMock.urlPathEqualTo("/data/2.5/weather")).willReturn(aResponse().withStatus(200)
.withHeader("Content-Type", "application/json; charset=utf-8").withBody("hier steht der Body")));
In der Methode "urlPathEqualTo" wird hinterlegt auf welche URL reagiert werden soll. Sobald also "/data/2.5/weather" vom abfragenden Programm aufgerufen wird, erfolgt eine Umleitung an den WireMock Server und wird nicht zur API durchgestellt.
In der "willReturn" Methode wird hinterlegt, was genau passieren soll, wenn besagte URL aufgerufen wird. Es erfolgt eine Antwort mit dem Status 200 und sowohl einem Header als auch einem Body.
Im Anschluss an diese Rule lässt sich dann der Test aufrufen und das Ergebnis vergleichen.
Beispiel:
@Test
public void howIsTheWetherTodayDefaultCity() throws IOException {
final String JSON_FILE_PATH = "weatherskill/cloudy_frankfurt.json";
final int PORT = wireMockRule.port();
final WeatherSkill weatherskill = new WeatherSkill(HOST + ":" + PORT);
wireMockRule.stubFor(get(WireMock.urlPathEqualTo("/data/2.5/weather")).withQueryParam("q", WireMock.equalTo(
"Frankfurt+am+Main")).withQueryParam("appid", WireMock.notMatching("")).willReturn(aResponse().withStatus(200)
.withHeader("Content-Type", "application/json; charset=utf-8").withBodyFile(JSON_FILE_PATH)));
String response = weatherskill.handle("wie wird das wetter heute");
assertEquals("Heute ist Mäßig bewölkt angesagt in Frankfurt am Main", response);
}
Der WeatherSkill greift auf die Open Weather API zurück und wertet anhand von bestimmten Phrasen das Wetter aus. Wie eingangs erwähnt, ist das Wetter immer verschieden und somit könnte der Test heute erfolgreich sein, aber morgen schon wieder fehlschlagen, nur weil die API ein anderes Ergebnis liefert.
Anstatt wie oben einfach nur einen Body zurückzuliefern, antwortet WireMock hier mit einer Datei, daher auch die "withBodyFile"-Methode.
Mit Dateien antworten
Für die Lesbarkeit und Analyse ist es deutlich einfacher, Dateien auszulagern und nicht direkt im Quellcode zu pflegen. WireMock unterstützt dies, in dem es standardmäßig auf "srctestresources__files"- Verzeichnis zugreift.
Beispielhaft sei hier ein Auszug aus einer der Open Weather API Antworten aufgezeigt.
All diese Daten würden den Testcode nur unübersichtlich gestalten und ließen sich auch schwierig austauschen. Insbesondere dann, wenn viele verschiedene solcher Ergebnisse zum Testen verwendet werden.
Fazit
WireMock ist ein sehr gut geeignetes Tool für die Testautomatisierung von APIs. Gerade wenn die zurückgelieferten API-Ergebnisse sich stetig ändern und dies ein ideales Werkzeug, um das Testen zu vereinfachen. Es spart besonders Zeit, wenn APIs sich noch in der Entwicklung befinden. Die Bedienung ist intuitiv und leicht erlernbar.
Für produktiven Testcode, bei dem WireMock zum Einsatz kam, empfehle ich mein Github Repository, speziell, das von meinem Voiceassistant.