Suche
REST-Anwendungen testen mit REST-assured

RESTlos glückliches REST-Testen

Oliver Fischer
©Shutterstock/amasterphotographer

Webservices sind inzwischen Standard. Auf HTTP-basierende REST-Schnittstellen mit JSON als Format zum Austausch von Daten sind State-of-the-Art. Doch wie können REST-Schnittstellen gut und effizient getestet werden? Dieser Artikel zeigt, wie sich REST-Schnittstellen mit dem Framework REST-assured zuverlässig testen lassen.

Der Erfolg von REST als Designparadigma für Schnittstellen ist ohne AJAX sowie JSON und die so mögliche Entstehung moderner, hoch interaktiver Webanwendungen nicht denkbar. Abgesehen davon besticht REST durch seine Interoperabilität, lose Kopplung und durch Möglichkeiten zur Performance-Steigerung. So etabliert REST zunehmend auch Einsatz zur Entwicklung von Schnittstellen in verteilten System und für öffentliche Webservice-Schnittstellen.

Softwareentwickler stehen aber damit vor der Aufgabe, REST-Schnittstellen effizient zu testen. Praktisch bedeutet dies, dass wir im Falle einer HTTP-REST-Schnittstelle mit JSON oder XML in der Lage sein müssen, über den kompletten Stack hinweg zu testen. Das schließt das Verhalten der HTTP-Schicht genauso mit ein, wie die Prüfung der gelieferten Daten. REST-Schnittstellen können daher nur richtig in einem Integrationstest geprüft werden.

Das Rüstzeug

Das Rüstzeug für einen solchen Integrationstest stellen zwei Frameworks. Das erste ist das an dieser Stelle schon öfters vorgestellte Arquillian [1], das es erlaubt, automatisierte Tests für Java-EE-Container zu schreiben. Arquillian übernimmt damit den Infrastrukturteil des Integrationstests wie den Start und Stop des Containers, das Deployment der Applikation sowie die Testausführung. Das vom Unternehmen Jayway entwickelte Framework REST-assured [2] ist für die Testlogik verantwortlich (Jayway dürfte einigen auch als Erfinder des Mocking-Frameworks Powermock bekannt sein). REST-assured stellt hierfür ein vom Behavior Driven Development beeinflusstes Fluent-API bereit, über das alle Tests einer REST-Schnittstelle in der Form „Gegeben-Erwarte-Wenn“ formuliert werden können.

Listing 1 zeigt einen minimalen Test, nach dessen Aufbau sich alle weiteren Tests mit REST-assured richten. In diesem Test wird als HTTP-Body „Hi“ an die URI http://localhost:8080/helloworld via HTTP-PUT geschickt und als Response mit dem HTTP-Status 200 erwartet.

@Test
public void getToResourceResultsIn200()
{
    RestAssured.given().body("Hi")
               .expect().status(200)
               .when().put("/helloworld");
}

Natürlich sind die in der Realität abzubildenden Operationen komplexer als in diesem Beispiel. REST-assured ermöglicht es jedoch, nach diesem Schema beliebig komplexe Anfragen zu stellen und Antworten zu überprüfen. Die nachfolgenden Beispiele beruhen auf einem kleinen, für diesen Artikel entwickelten Showcase, bei dem via REST Nachrichten an einen imaginären Nachrichtendienst übergeben werden, wobei die Daten für Anfrage und Body immer als JSON-Dokumente vorliegen.

Listing 2 zeigt ein etwas komplexeres Beispiel, bei dem eine neue Nachricht an den Dienst gesendet wird. Zum einen wird in diesem Beispiel explizit der Content-Type der Anfrage gesetzt. Zum anderen wird die Nachricht als POJO übergeben und muss daher von REST-assured vor der Anfrage nach JSON umgewandelt werden. Hierfür erlaubt REST-assured die explizite Angabe des zu verwendenden Mappers. Dieser kann entweder global gesetzt oder, wie in diesem Beispiel, explizit übergeben werden, wenn er das Interface ObjectMapper implementiert. Ist die Standardkonfiguration ausreichend, kann auch einfach über bereitstellte Enum-Konstanten angegeben werden, ob Jackson 1 oder 2, JAXB oder GSON verwendet werden soll.

public void canPostNewMessage() throws Exception
{
    long ts = System.currentTimeMillis();

    Message outMsg = new Message();
    Member from = new Member("Peter Kowalke", "kowa");
    Member to = new Member("Richy Paschulke", "richy");

    outMsg.setMessageId(ts);
    outMsg.setMessage("Hallo");
    outMsg.setFrom(from);
    outMsg.setTo(to);

    RestAssured.given()
               .contentType(ContentType.JSON)
               .body(outMsg, new MapperBuilder().build())

               .expect()
               .statusCode(201)
               .contentType(ContentType.JSON)
               .header("Location", "/messages/" + ts)

               .when()
               .post("/messages");
}

Aufmacherbild: 3d man showing thumbs up von Shutterstock / Urheberrecht: amasterphotographer

[ header = Seite 2: Konfiguration und Wiederverwendung ]

Die Realität ist komplex

Beim Testen einer REST-Schnittstelle liegt das Hauptaugenmerk auf der Validierung der Antwort auf eine Anfrage, sodass REST-assured hier besonders seine Flexibilität unter Beweis stellt. Wie in Listung 2 zu sehen ist, lassen sich Header-Felder ebenso prüfen wie Cookies. Hierbei kann man entweder direkt die erwarteten Werte angeben oder auf die Hamcrest-Matcher-Bibliothek zurückgreifen. Wären für die Anfrage in Listing 2 alle Status-Codes von 200 bis 202 zulässig, ließe sich das als .statusCode(allOf(greaterThanOrEqualTo(200),lessThanOrEqualTo(202))) ausdrücken. Für solch lange Ausdrücke sollte jedoch ein eigener Hamcrest-Matcher implementiert werden.

Das Prüfen der Header-Felder der Antwort ist einfach, da der Header des HTTP-Protokolls simpel aufgebaut ist und im Grunde nur Text enthält. Eine größere Herausforderung stellt die Validierung des Bodys der Antwort dar. Dieser kann aus JSON-, XML-Dokumenten oder gar aus binären Daten bestehen. Im einfachsten Falle ist die Antwort lediglich Text, dessen Inhalt sich mittels des Ausdrucks expext().body(equalTo(„Hello World!“)) überprüfen ließe. REST-assured erleichtert aber auch die Überprüfung von XML- und JSON-Dokumenten. Gibt es für XML-Dokumente ein Schema, lässt sich die Antwort durch .body(matchesXsd(schema)) auf Schemakonformität prüfen. Aber auch XPath wird unterstützt, um auf einzelne Knoten eines XML-Dokuments für eine Überprüfung zugreifen zu können. So checkt .body(hasXPath(„/user/name[text()=’Richy‘]“)) den Inhalt des Knotens /user/name. Die gleiche Erwartung kann auch mittels .body(„user.name“, equalTo(„Richy“)) überprüft werden.

Werden Erwartungen in dieser Form ausgedrückt, kann REST-assured über Pfade auf Werte in JSON-Dokumenten zugreifen. Hierbei ist es nicht notwendig anzugeben, ob das erhaltene Dokument als XML oder JSON vorliegt. REST-assured stellt dies selber fest, wofür es die eigens entwickelten Bibliotheken JSON Path und XML Path nutzt, die auch unabhängig von REST-assured in eigenen Projekten verwendet werden können. Dies ist vor allem dann möglich, wenn es um den einfachen Zugriff auf Daten in XML- und JSON-Dokumenten geht.

Konfiguration und Wiederverwendung

Beim Lesen der Beispiele dürfte aufgefallen sein, dass weder Host noch Port beim Zugriff auf die REST-URIs angegeben wurden. REST-assured folgt hier dem Convention-Over-Configuration-Ansatz und geht davon aus, dass beim Testen auf http://localhost:8080/ zugegriffen wird und alle angegebenen Pfade immer relativ hierzu sind. Über statische Variablen der Klasse RestAssured können sämtliche Voreinstellungen an die eigene Umgebung angepasst werden.

Im Idealfall sollten Tests immer nur einen einzelnen Aspekt überprüfen. So sollte die Prüfung auf den Content-Type und den zurückgelieferten HTTP-Status-Code in zwei getrennten Tests erfolgen, denn sobald die erste Annahme nicht erfüllt ist, wird der jeweilige Test mit einer Exception beendet. Treffen weitere Erwartungen nicht zu, können diese nicht mehr überprüft werden – schnell befindet man sich in einem Try-And-Error-Loop, bis alle Tests wieder grün sind. Viele Tests zu schreiben und konsistent zu halten ist eine Herausforderung, die REST-assured dadurch unterstützt, dass für Anfragen und Antworten wiederverwendbare Spezifikationen angelegt werden können. Mit diesen kann ein Testfall vorkonfiguriert und anschließend nach Bedarf variiert werden. In Listing 3 wird davon Gebrauch gemacht.

public void requestAndResponseSpecificationUsage()
{
    long ts = System.currentTimeMillis();

    Message outMsg = new Message();
    Member from = new Member("Peter Kowalke", "kowa");
    Member to = new Member("Richy Paschulke", "richy");

    outMsg.setMessageId(ts);
    outMsg.setMessage("Jetzt mit Spezifikation!");
    outMsg.setFrom(from);
    outMsg.setTo(to);

    RestAssured.given()
               .contentType(ContentType.JSON)
               .body(outMsg, JACKSON_2)
               .expect()
               .statusCode(201)
               .when()
               .post(PATH_TO_MSG_COLLECTION);

    ResponseSpecBuilder responseBuilder = new ResponseSpecBuilder();
    RequestSpecBuilder requestBuilder = new RequestSpecBuilder();

    requestBuilder.setContentType(ContentType.JSON)
                  .addHeader("Accept", ContentType.JSON.getAcceptHeader());

    responseBuilder.expectStatusCode(200)
                   .expectContentType(ContentType.JSON);

    ResponseSpecification specOfResponse = responseBuilder.build();
    RequestSpecification specOfRequest = requestBuilder.build();

    RestAssured.given()
               .spec(specOfRequest)
               .expect()
               .spec(specOfResponse)
               .when()
               .get(PATH_TO_MSG_COLLECTION + "/{id}", ts);
}

[ header = Seite 3: Zusammenspiel mit Arquillian ]

Zusammenspiel mit Arquillian

Wie schon anfangs erwähnt, kann Arquillian die Bereitstellung der Testumgebung für Integrationstests übernehmen, da es erlaubt, den gewünschten Container transparent während der Testausführung zu starten und das Deployment der zu testenden REST-Applikation vorzunehmen. Eine grundlegende Einführung in Arquillian haben im JavaMagazin 1.2011 bereits Michael Schütz und Alphonse Bendt vorgenommen, auf die hier verwiesen sei. Bei der Verwendung von Arquillian im Zusammenspiel mit REST-assured sollte darauf geachtet werden, dass die Tests als Clients durchgeführt werden. Hierfür sind die Testklassen mit der Annotation @RunAsClient zu versehen. Erfolgt dies nicht, werden die Tests von Arquillian zusammen mit der Applikation in den genutzten Container deployt und dort ausgeführt. Wird die zu testende Applikation als WebArchive (Arquillians Entsprechung eines Wars) deployt, kann die Base-URL der Applikation mittels der Annotation @ArquillianResource in den Testfall injiziert werden. Dadurch besteht die Möglichkeit, die Konfiguration von REST-assured dynamisch an das Deployment anzupassen.

Fazit

Arqullian und REST-assured dürften momentan in der Java-Welt das Dream-Team darstellen, wenn es um das effiziente Testen von REST-Schnittstellen geht. Dabei erweist sich REST-assured durch gekonnte Integration von Bibliotheken wie Hamcrest als flexibel und anpassungsfähig und deckt zudem weit mehr Anwendungsfälle ab, als in diesem Artikel zu zeigen möglich ist. Zusätzlich zu diesem Artikel steht auf Bitbucket ein Maven-basierter Showcase [3] zum Zusammenspiel von Arquillian und REST-assured bereit.

Geschrieben von
Oliver Fischer
Oliver Fischer
Oliver B. Fischer ist Senior Software Engineer bei der E-Post Development GmbH und engagiert sich in der JUG Berlin-Brandenburg.
Kommentare
  1. Karel Piwko2013-10-21 07:13:42

    RESTAssured works even better if combined with Arquillian Spock extension and tests are written in Groovy.

    A nice example is available here:

    https://github.com/kpiwko/arquillian-safari/blob/master/arquillian-rest-scenario/src/test/groovy/com/acme/example/test/MemberSpecification.groovy

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht.