07.12.2013
Testen einer REST-Schnittstelle mit REST Assured
In dem folgenden Artikel wird beschrieben, wie ein REST-Service, der mit Spring MVC implementiert wurde, mittels REST Assured getestet werden kann. Dabei werden die Requests POST, PUT, GET und DELETE verwendet. Bei den REST Assured Tests werden beim Überprüfen der JSON-Responses zwei Möglichkeiten vorgestellt. Als erstes ein eigener Hamcrest-Matcher und als zweites Möglichkeit die Überprüfung mittels JSONPath. Als Testbeispiel dient ein einfacher REST-Service, den ich in dem Artikel Spring MVC: Ein minimales REST-Beispiel beschrieben habe.
Maven und REST Assured
Damit REST Assured und JSONPath in den JUnit-Tests zur Verfügung steht, muss die Datei pom.xml
um folgende Dependencies erweitert werden.
<dependency> <groupId>com.jayway.restassured</groupId> <artifactId>rest-assured</artifactId> <version>2.0.1</version> <scope>test</scope> </dependency> <dependency> <groupId>com.jayway.restassured</groupId> <artifactId>json-path</artifactId> <version>2.0.1</version> <scope>test</scope> </dependency>
Allgemeines zu REST Assured Tests
Prinzipiell sind REST Assured Tests immer nach dem gleichen AAA-Muster aufgebaut.
given(). ARRANGE when(). ACT then(). ASSERT
Mit given()
wird ein Testszenario beschrieben (ARRANGE). Mit when()
wird beschrieben, welche Operationen ausgeführt werden soll (ACT). Und mit then()
wird überprüft, ob das Ergebnis den Annahmen entspricht (ASSERT).
Hinweis: Seit REST Assured 2.x wird diese Notation verwendet. In der vorangegangenen Version wurde noch expect()
anstatt des Aufrufs then()
verwendet.
Setup
Für die, in den folgenden Abschnitten beschriebenen Tests, werden folgende Variablen und Imports benötigt.
package org.hameister.spring; import static com.jayway.restassured.RestAssured.given; import static com.jayway.restassured.path.json.JsonPath.from; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.isIn; import static org.hamcrest.Matchers.nullValue; import static org.junit.Assert.assertThat; import java.util.List; import org.hamcrest.Description; import org.hamcrest.Matcher; import org.hamcrest.TypeSafeMatcher; import org.junit.Before; import org.junit.BeforeClass; import org.junit.Test; import com.jayway.restassured.RestAssured; import com.jayway.restassured.builder.RequestSpecBuilder; import com.jayway.restassured.builder.ResponseSpecBuilder; import com.jayway.restassured.http.ContentType; import com.jayway.restassured.response.Response; public class CustomerControllerRESTassuredTest { private static final String CUSTOMER_ID_DOES_NOT_EXIST = "doesNotExist"; private static final String ANY_NAME = "anyName"; private static final String INVALID_ARGUMENT = "invalidArgument"; RequestSpecBuilder requestSpecBuilder = new RequestSpecBuilder(); @BeforeClass public static void setupConnection() { RestAssured.baseURI = "http://localhost"; RestAssured.port = 2001; RestAssured.basePath = "/spring/customers"; } @Before public void setup() throws Exception { requestSpecBuilder.setContentType(ContentType.JSON).addHeader("Accept", ContentType.JSON.getAcceptHeader()); }
In der Funktion setupConnection
wird REST Assured mitgeteilt, wo der Server zu erreichen ist.
Die Funktion setup
definiert einen RequestSpecBuilder
, der festlegt, dass in den folgenden Tests bei einem Request immer JSON verwendet wird. Außerdem wird der Accept-Header gesetzt. Mit dem RequestSpecBuilder
spart man sich, dass diese Dinge bei jedem Test erneut definiert werden müssen. Wenn man beim Schreiben von Tests merkt, dass Requests immer gleich aufgebaut sind, dann bietet es sicht an, diese in den RequestSpecBuilder
auszulagern. Für die Antworten (Response) wird ein ResponseSpecBuilder
zur Verfügung gestellt.
Tests mit REST Assured
Bei den folgenden Tests mit REST Assured werden zunächst die Basisfunktionalitäten der REST-Schnittstelle mit POST, GET, PUT und DELETE überprüft.
Anschließend werden verschiedene HTTP-Statuscodes und die Fehlermeldungen, die von den ExceptionHandlern
von Spring MVC erstellt wurden, überprüft. Dazu werden gezielt Fehler durch falsche Parameter beim PUT, GET und DELETE erzeugt.
Abschließend wird der Inhalt einer Liste mit Objekten vom Typ Customer
überprüft. Dazu wird in einem Testfall ein eigener Hamcrest-Matcher und in dem anderen Testfall ein JSONPath verwendet.
Test eines POST-Requests
In diesem Test wird überprüft, ob der Return-Code 201 (Created) zurück geliefert wird und ob die location
im Header gesetzt wurde.
Die Zeile .log().all()
sorgt für eine Debug-Ausgabe auf der Konsole und dient hier nur als Hinweis, dass die Möglichkeit besteht, Logausgaben zu erzeugen.
@Test public void createACustomerShouldReturnHTTP201() throws Exception { given() .spec(requestSpecBuilder.build()) .log().all() .when() .post("/") .then() .statusCode(201) .headers("location", containsString("/customers/")); }
Test eines PUT-Requests
Bei diesem Test, wird ein Customer angelegt und das Attribute Name
geändert. Anschließend wird überprüft, ob der Statuscode 204 (No Content) zurückgeliefert wurde.
@Test public void updateACustomerShouldReturnHTTP204() throws Exception { String customerId = createCustomer(); ResponseSpecBuilder noContentInResponse = new ResponseSpecBuilder(); noContentInResponse.expectBody(is("")).expectContentType(""); given() .spec(requestSpecBuilder.build()) .body("{\"Name\":\""+ANY_NAME+"\"}") .pathParam("id", customerId) .when() .put("/{id}") .then() .statusCode(204) .spec(noContentInResponse.build()); }
Die Methode createCustomer()
wird in den folgenden Tests verwendet, um einen Customer
anzulegen. Prinzipiell wird hier ein POST an die REST-Schnittstelle geschickt und die location
im Header ausgewertet, um die customerId
zu bestimmen.
private String createCustomer() { Response response = given().spec(requestSpecBuilder.build()).body("").post("/"); String customerLocation = response.header("location"); return customerLocation.substring(customerLocation.lastIndexOf("/")+1, customerLocation.length()); }
Test eines GET-Requests
Bei diesem Test wird mit einem HTTP-GET überprüft, ob ein initial angelegt Customer
eine gültige id
hat und ob die anderen Werte auf null
gesetzt sind.
@Test public void getACustomer() throws Exception { String customerId = createCustomer(); given() .pathParam("id", customerId) .when() .get("/{id}") .then() .statusCode(200) .contentType(ContentType.JSON) .body("id", is(customerId)) .body("name", is(nullValue())) .body("created", is(nullValue())); }
Test eines DELETE-Request
Der Test überprüft, ob sich ein Customer
mit einem HTTP-DELETE wieder löschen läßt.
@Test public void deleteACustomer() throws Exception { String customerId = createCustomer(); given() .spec(requestSpecBuilder.build()) .pathParam("id", customerId) .log().headers() .when() .delete("/{id}") .then() .statusCode(200); }
Test von HTTP-Codes eines PUT
Die folgenden beiden Testfälle überprüfen, ob bei invaliden Parametern der richtige HTTP-Statuscode und die erwartete Fehlermeldung zurückgeliefert wird.
@Test public void updateANotExistingCustomerShouldReturnHTTP404() throws Exception { given() .spec(requestSpecBuilder.build()) .body("{\"Name\":\""+ANY_NAME+"\"}") .pathParam("id", CUSTOMER_ID_DOES_NOT_EXIST) .when() .put("/{id}") .then() .statusCode(404) .body(is("{\"reason\":\"Customer with id '"+CUSTOMER_ID_DOES_NOT_EXIST+"' not found.\"}")); } @Test public void updateACustomerWithInvalidKeyShouldReturnHTTP400() throws Exception { String customerId = createCustomer(); given() .spec(requestSpecBuilder.build()) .body("{\""+INVALID_ARGUMENT+"\":\""+ANY_NAME+"\"}") .pathParam("id", customerId) .when() .put("/{id}") .then() .statusCode(400) .body(is("{\"reason\":\"The mandatory argument 'Name' is missing in the request.\"}")); }
Test von HTTP-Codes eines GET
Dieser Testfall überprüft, ob die Suche nach einem Customer
, der nicht existiert, die richtige Fehlermeldung und den Statuscode 404 zurückliefert.
@Test public void getANotExistingCustomerShouldReturnHTTP404() throws Exception { given() .spec(requestSpecBuilder.build()) .pathParam("id", CUSTOMER_ID_DOES_NOT_EXIST) .when() .get("/{id}") .then() .statusCode(404) .body(is("{\"reason\":\"Customer with id '"+CUSTOMER_ID_DOES_NOT_EXIST+"' not found.\"}")); }
Test von HTTP-Codes eines DELETE
Dieser Test überprüft, ob die Fehlermeldung, die beim Löschen eines Customer
, der nicht existiert, richtig ist.
@Test public void deleteANotExistingCustomerShouldReturnHTTP404() throws Exception { given() .spec(requestSpecBuilder.build()) .pathParam("id", CUSTOMER_ID_DOES_NOT_EXIST) .when() .delete("/{id}") .then() .statusCode(404) .body(is("{\"reason\":\"Customer with id '"+CUSTOMER_ID_DOES_NOT_EXIST+"' not found.\"}")); }
Test des JSON-Contents mit JSONPath
Mit einem JSONPath läßt sich die JSON-Response relativ einfach parsen. D.h. es ist mit wenigen Kommandos möglich den Inhalt nach bestimmten Werten zu durchsuchen. In dem Test wird einfach nur überprüft, ob ein Customer
mit einer bestimmten customerId
in der Rückgabeliste enthalten ist. Mehr zu JSONPath findet man in diesem Blogpost: Parsing JSON documents with REST Assured JsonPath und die der Dokumentation von REST Assured JSON (using JsonPath).
@Test public void getAllCustomersWithJSONPath() throws Exception { String customerId = createCustomer(); Response response = given() .log().all() .when() .get("/") .then() .statusCode(200) .extract() .response(); // Test with JsonPath List<String> idList = from(response.body().asString()).getList("id", String.class); assertThat(customerId, isIn(idList)); }
Eine Liste mit weiteren Funktionalitäten der API und Beispiele findet man in der JavaDoc der Klasse JsonPath.
Test des JSON-Contents mit einem Hamcrest-Matcher
In diesem Test wird ein eigener Hamcrest-Matcher erstellt, um zu überprüfen, ob die Rückgabeliste die customerId
enthält.
Um einen Hamcrest-Matcher zu erstellen, müssen die beiden Methoden des TypeSafeMatcher
implementiert werden. In dem Beispiel wird der String
mit der JSON-Response einfach mit einem indexOf
untersucht.
@Test public void getAllCustomersWithMatcher() throws Exception { String customerId = createCustomer(); given() .log().all() .when() .get("/") .then() .statusCode(200) .body(containsCustomerId(customerId)); } private Matcher<String> containsCustomerId(String customerId) { final String id = customerId; return new TypeSafeMatcher<String>() { @Override public void describeTo(Description description) { description.appendText("customerId="+id+" is JSON response. ({\"id\":\""+id+"\",\"name\":null,\"created\":null})"); } @Override protected boolean matchesSafely(String jsonResponse) { if(jsonResponse.indexOf("\"id\":\""+id+"\"")>=0) { return true; } return false; } }; }
Beispielcode
Der komplette Quellcode kann bei GitHub unter URL https://github.com/hameister/CustomerService heruntergeladen werden.
Dieser Stand kann mit git checkout cdb7dae6ee183223089a6ff3d105467cb886faf9
ausgecheckt werden.
Anmerkungen
Damit die Tests ausführbar sind, muss der Tomcat-Server laufen und die Applikation mit dem zu testenden REST-Service muss deployed sein. Für automatisierte Tests kann das ein Nachteil sein. Deshalb bietet beispielsweise Arquillian eine Möglichkeit diesen Schwachpunkt zu beseitigen.