Q Q Q Q Q Q Q Q Q Q Q Q Q Q Q
blog-icon Blog

3 Min. Lesezeit

WireSpock – REST-Service Clients mit Spock und WireMock testen

In einem früheren Beitrag habe ich über die Nutzung des Spock-Frameworks für das explorative Testen von Open-Source-Software geschrieben. In diesem Beitrag möchte ich eine elegante Technologie-Integration zwischen Spock und dem WireMock-Framework zum Testen Ihrer REST-Service Client Komponenten vorstellen. Dies ist besonders beim Testen von Microservice-basierten Architekturen nützlich, da man die einzelnen Service-Integrationen testen möchte, ohne alle abhängigen Dienste (Collaborators) starten zu müssen.

Einführung in WireMock

Wie auf der eigenen Website angegeben, ist WireMock ein „Web-Service-Test-Double für alle Fälle“. Es unterstützt das Stubbing und Mocking von HTTP-Aufrufen sowie die Überprüfung von Anfragen (Request Verification), das Aufzeichnen und Abspielen von Stubs (Record & Playback), die Simulation von Fehlern (Fault Injection) und vieles mehr. Es startet tatsächlich einen kleinen eingebetteten HTTP-Server, sodass Ihr Code und Ihre Tests auf Protokollebene mit ihm interagieren.

Der komfortabelste Weg, WireMock in Ihren Testfällen zu nutzen, ist über eine JUnit 4.x Rule, die den Lebenszyklus des Startens und Stoppens des Mock-Servers vor und nach jedem Test verwaltet. Es ist auch eine ClassRule verfügbar, falls es ausreicht, dieselbe WireMock-Instanz über die gesamte Laufzeit des Testfalls hinweg zu verwenden. Die offizielle Dokumentation zur Rule finden sie hier.

Das Gute daran ist, dass sie die WireMock-Rule in Ihrer Spock-Spezifikation genau wie in einem gewöhnlichen JUnit-basierten Test verwenden können. Keine Magie an dieser Stelle. Schauen wir uns das folgende Beispiel an.

class BookServiceClientSpec extends Specification {

    @Rule
    WireMockRule wireMockRule = new WireMockRule(18080)

    @Shared
    def client = new BookServiceClient("http://localhost:18080")

    def "Find all books using a WireMock stub server"() {
        given: "a stubbed GET request for all books"
        // TODO

        when: "we invoke the REST client to find all books"
        def books = client.findAll()

        then: "we expect two books to be found"
        books.size() == 2

        and: "the mock to be invoked exactly once"
        // TODO
    }
} 

Zuerst wird die JUnit-WireMock-Rule erstellt und so initialisiert, dass sie auf Port 18080 lauscht. Als Nächstes wird die zu testende REST-Client Komponente erzeugt und so konfiguriert, dass sie auf den lokalen WireMock-Server zugreift. Die Testmethode selbst macht noch nicht viel. Damit sie funktioniert, müssen wir die Antwort für die findAll()-Abfrage stubben und überprüfen, ob der Mock einmal aufgerufen wurde. Bevor wir fortfahren, werfen wir einen Blick auf die Test-Abhängigkeiten, die zum Kompilieren und Ausführen des Beispiels erforderlich sind.

dependencies {
    testCompile 'junit:junit:4.12'
    testCompile 'org.spockframework:spock-core:1.0-groovy-2.4'

    testCompile 'com.github.tomakehurst:wiremock:1.57'
    testCompile 'com.github.tomjankes:wiremock-groovy:0.2.0'
}

WireMock groovy machen

Die letzte Abhängigkeit ist eine kleine Groovy-Anbindung für WireMock, die hervorragend mit Spock zusammenspielt. Sie ermöglicht eine prägnantere Syntax für Stubbing und Verifikation, anstatt die standardmäßige API von WireMock mit statischen Imports nutzen zu müssen. Das folgende Beispiel veranschaulicht die Idee.

def wireMock = new WireMockGroovy(18080)

def "Find all books using a WireMock stub server"() {
    given: "a stubbed GET request for all books"
    wireMock.stub {
        request {
            method "GET"
            url "/book"
        }
        response {
            status 200
            body """[
                      {"title": "Book 1", "isbn": "4711"},
                      {"title": "Book 2", "isbn": "4712"}
                    ]
                 """
            headers { "Content-Type" "application/json" }
        }
    }

    when: "we invoke the REST client to find all books"
    def books = client.findAll()

    then: "we expect two books to be found"
    books.size() == 2

    and: "the mock to be invoked exactly once"
    1 == wireMock.count {
        method "GET"
        url "/book"
    }
}

Zuerst erstellen wir die WireMockGroovy-Anbindung, um gestubbte Anfragen und Antworten zu definieren. Der stub-Closure nimmt die Definitionen der REST-Anfrage und -Antwort mithilfe der WireMock-JSON-API entgegen. Wie sie sehen können, können wir den Antwort-Body sogar als mehrzeiligen Inline-JSON-GString angeben. Schließlich überprüfen wir, ob die Anzahl der Aufrufe für die erwartete Anfrage korrekt ist.

Das Angeben der Antworten direkt im Code (inline) ist – besonders bei großen Antwortstrukturen – nicht sehr wartungsfreundlich. Eine bessere Alternative besteht darin, den Antwort-Body in eine separate Datei auszulagern. Die Datei muss in einem Ordner namens __files innerhalb von src/test/resources liegen. Der Wert von bodyFileName bezieht sich auf das Verzeichnis __files und kann beliebigen Inhalt enthalten. Über diesen Mechanismus könnten sie sogar Binärdateien wie JPEGs zurückgeben.

response {
    status 200
    bodyFileName "books.json"
    headers { "Content-Type" "application/json" }
}

Eine weitere Möglichkeit, den Antwort-Body anzugeben, ist die Verwendung von normalen Java- oder Groovy-Objekten, die automatisch nach JSON serialisiert werden.

response {
    status 200
    jsonBody new Book(title: "WireSpock Showcase", isbn: "4713")
    headers { "Content-Type" "application/json" }
}

Die Stubbing-Fähigkeiten von WireMock sind sehr mächtig. Sie können verschiedene Vergleiche (Matchings) auf der URL, den Request-Headern, den Query-Parametern oder dem Request-Body durchführen, um die passende Antwort zu ermitteln. Werfen sie einen Blick in die WireMock-Stubbing-Dokumentation für eine vollständige Beschreibung aller Features.

Bleibt nur noch eines zu sagen: Test long and prosper with Spock!

Referenzen

Ein Beitrag von

Mario-Leander Reimer

ist Managing Director / CTO bei QAware. Er ist Spezialist für den Entwurf, die Umsetzung und den Betrieb von verteilten System- und Softwarearchitekturen auf Basis von Open-Source Bausteinen. [...]