REST Assured: Was wir aus fünf Jahren mit dem Tool gelernt haben

REST Assured - DSL zum Testen von REST-Diensten, das in Java-Tests eingebettet ist. Diese Lösung erschien vor mehr als neun Jahren und ist aufgrund ihrer Einfachheit und praktischen Funktionalität populär geworden.


In DINS haben wir mehr als 17.000 Tests damit geschrieben und in den fünf Jahren der Verwendung sind wir auf viele Fallstricke gestoßen, die nicht direkt nach dem Importieren der Bibliothek in das Projekt herausgefunden werden können: ein statischer Kontext, Verwirrung in der Reihenfolge, in der Filter auf die Abfrage angewendet werden, Schwierigkeiten bei der Strukturierung des Tests.


Dieser Artikel befasst sich mit solchen impliziten Funktionen von REST Assured. Sie müssen berücksichtigt werden, wenn die Möglichkeit besteht, dass die Anzahl der Tests im Projekt schnell zunimmt - damit Sie sie später nicht neu schreiben müssen.


Bild


Was testen wir?


DINS ist an der Entwicklung der UCaaS-Plattform beteiligt. Insbesondere entwickeln und testen wir die API, die RingCentral selbst verwendet und Drittentwicklern zur Verfügung stellt .


Bei der Entwicklung einer API ist es wichtig sicherzustellen, dass sie ordnungsgemäß funktioniert. Wenn Sie sie jedoch herausgeben, müssen Sie viel mehr Fälle überprüfen. Daher werden jedem neuen Endpunkt Dutzende und Hunderte von Tests hinzugefügt. Tests werden in Java geschrieben, TestNG wird als Testframework ausgewählt und REST Assured wird für API-Anforderungen verwendet.


Wenn REST Assured davon profitiert


Wenn Sie nicht die gesamte API gründlich testen möchten, können Sie dies am einfachsten mit REST Assured tun. Es eignet sich gut zur Überprüfung der Reaktionsstruktur, der PVD und der Rauchtests.


So sieht ein einfacher Test aus, der überprüft, ob der Endpunkt beim Zugriff den Status 200 OK anzeigt:


given() .baseUri("http://cookiemonster.com") .when() .get("/cookies") .then() .assertThat() .statusCode(200); 

Die angegebenen Schlüsselwörter, when und then bilden die Anfrage: given bestimmt, was in der Anfrage gesendet wird, when - mit welcher Methode und an welchen Endpunkt wir die Anfrage senden und then - wie die empfangene Antwort überprüft wird. Darüber hinaus können Sie den Antworttext in Form eines Objekts vom Typ JsonPath oder XmlPath und dann die empfangenen Daten verwenden.


Echte Tests sind normalerweise größer und komplizierter. Header, Cookies, Autorisierung und Anfragetext werden zu Anfragen hinzugefügt. Wenn die zu testende API nicht aus Dutzenden eindeutiger Ressourcen besteht, für die jeweils spezielle Parameter erforderlich sind, sollten Sie vorgefertigte Vorlagen irgendwo speichern, um sie später einem bestimmten Aufruf im Test hinzuzufügen.


Dafür gibt es in REST Assured:


  • RequestSpecification / ResponseSpecification ;
  • Grundkonfiguration;
  • Filter.

RequestSpecification und ResponseSpecification


Mit diesen beiden Klassen können Sie die Anforderungsparameter und Erwartungen aus der Antwort ermitteln:


 RequestSpecification requestSpec = given() .baseUri("http://cookiemonster.com") .header("Language", "en"); requestSpec.when() .get("/cookiesformonster") .then() .statusCode(200); requestSpec.when() .get("/soup") .then() .statusCode(400); 

 ResponseSpecification responseSpec = expect() .statusCode(200); given() .expect() .spec(responseSpec) .when() .get("/hello"); given() .expect() .spec(responseSpec) .when() .get("/goodbye"); 

Eine Spezifikation wird in mehreren Aufrufen, Tests und Testklassen verwendet, je nachdem, wo sie definiert ist - es gibt keine Einschränkung. Sie können einer einzelnen Anforderung sogar mehrere Spezifikationen hinzufügen. Dies ist jedoch eine potenzielle Problemquelle :


 RequestSpecification requestSpec = given() .baseUri("http://cookiemonster.com") .header("Language", "en"); RequestSpecification yetAnotherRequestSpec = given() .header("Language", "fr"); given() .spec(requestSpec) .spec(yetAnotherRequestSpec) .when() .get("/cookies") .then() .statusCode(200); 

Anrufliste:


 Request method: GET Request URI: http://localhost:8080/ Headers: Language=en Language=fr Accept=*/* Cookies: <none> Multiparts: <none> Body: <none> java.net.ConnectException: Connection refused (Connection refused) 

Es stellte sich heraus, dass alle Header zum Aufruf hinzugefügt wurden, aber der URI wurde plötzlich zu localhost - obwohl er in der ersten Spezifikation hinzugefügt wurde.


Dies geschah aufgrund der Tatsache, dass REST Assured Überschreibungen für Anforderungsparameter unterschiedlich behandelt (dasselbe gilt für die Antwort). Überschriften oder Filter werden der Liste hinzugefügt und dann der Reihe nach angewendet. Es kann nur einen URI geben, daher wird der letzte angewendet. Es wurde in der zuletzt hinzugefügten Spezifikation nicht angegeben. Daher überschreibt REST Assured es mit dem Standardwert (localhost).


Wenn Sie der Anforderung eine Spezifikation hinzufügen, fügen Sie eine hinzu . Der Rat scheint offensichtlich, aber wenn das Projekt mit Tests wächst, erscheinen Hilfsklassen und grundlegende Testklassen, bevor Methoden in ihnen erscheinen. Es wird schwierig, den Überblick darüber zu behalten, was mit Ihrer Anfrage tatsächlich passiert, insbesondere wenn mehrere Personen gleichzeitig Tests schreiben.


Grundlegende REST-versicherte Konfiguration


Eine andere Möglichkeit, Vorlagenabfragen in REST Assured durchzuführen, besteht darin, die Grundkonfiguration zu konfigurieren und die statischen Felder der RestAssured-Klasse zu definieren:


 @BeforeMethod public void configureRestAssured(...) { RestAssured.baseURI = "http://cookiemonster.com"; RestAssured.requestSpecification = given() .header("Language", "en"); RestAssured.filters(new RequestLoggingFilter(), new ResponseLoggingFilter()); ... } 

Werte werden der Anforderung jedes Mal automatisch hinzugefügt. Die Konfiguration wird mit den Anmerkungen @BeforeMethod in TestNG und @BeforeEach in JUnit kombiniert, sodass Sie sicher sein können, dass jeder Test, den Sie ausführen, mit denselben Parametern beginnt.


Die Konfiguration kann jedoch zu Problemen führen, da sie statisch ist .


Beispiel: Vor jedem Test nehmen wir einen Testbenutzer, holen ein Autorisierungstoken für ihn und fügen es dann über AuthenticationScheme oder einen Autorisierungsfilter zur Grundkonfiguration hinzu. Solange die Tests in einem einzigen Thread ausgeführt werden, funktioniert alles.
Wenn es zu viele Tests gibt, führt die übliche Entscheidung, ihre Ausführung in mehrere Threads aufzuteilen, dazu, dass ein Code neu geschrieben wird, damit ein Token von einem Thread nicht in den benachbarten fällt.


REST-versicherte Filter


Filter ändern sowohl Anforderungen vor dem Senden als auch Antworten, bevor sie die Einhaltung der angegebenen Erwartungen überprüfen. Anwendungsbeispiel - Hinzufügen von Protokollierung oder Autorisierung:


 public class OAuth2Filter implements AuthFilter { String accessToken; OAuth2Filter(String accessToken) { this.accessToken = accessToken; } @Override public Response filter(FilterableRequestSpecification requestSpec, FilterableResponseSpecification responseSpec, FilterContext ctx) { requestSpec.replaceHeader("Authorization", "Bearer " + accessToken); return ctx.next(requestSpec, responseSpec); } } 

 String accessToken = getAccessToken(username, password); OAuth2Filter auth = new OAuth2Filter(accessToken); given() .filter(auth) .filter(new RequestLoggingFilter()) .filter(new ResponseLoggingFilter()) ... 

Filter, die der Anforderung hinzugefügt werden, werden in LinkedList gespeichert. Bevor Sie eine Anfrage stellen, ändert REST Assured diese, indem Sie die Liste durchgehen und einen Filter nach dem anderen anwenden. Dann wird dasselbe mit der Antwort gemacht, die kam.


Die Reihenfolge der Filter ist wichtig . Diese beiden Abfragen führen zu unterschiedlichen Protokollen: Die erste gibt den Autorisierungsheader an, die zweite - Nr. In diesem Fall wird der Header zu beiden Anforderungen hinzugefügt. Nur im ersten Fall fügt REST Assured vor der Anmeldung zuerst eine Autorisierung hinzu und im zweiten Fall umgekehrt.


 given() .filter(auth) .filter(new RequestLoggingFilter()) … given() .filter(new RequestLoggingFilter()) .filter(auth) 

Zusätzlich zu der üblichen Regel, dass Filter in der Reihenfolge angewendet werden, in der sie hinzugefügt werden, besteht weiterhin die Möglichkeit, Ihren Filter durch Implementierung der OrderedFilter Schnittstelle zu priorisieren. Hier können Sie eine spezielle numerische Priorität für den Filter festlegen, die über oder unter dem Standardwert (1000) liegt. Filter mit einer höheren Priorität werden früher als gewöhnlich ausgeführt, mit einer Priorität darunter - danach.


Natürlich können Sie hier verwirrt werden und versehentlich die beiden Filter auf dieselbe Priorität setzen, z. B. 999. Dann wird der zuvor hinzugefügte Filter zuerst auf die Anforderung angewendet.


Nicht nur Filter


Die Autorisierung über Filter ist oben dargestellt. Neben dieser Methode in REST Assured gibt es über AuthenticationScheme noch eine andere:


 String accessToken = getAccessToken(username, password); OAuth2Scheme scheme = new OAuth2Scheme(); scheme.setAccessToken(accessToken); RestAssured.authentication = scheme; 

Dies ist eine veraltete Methode. Stattdessen sollten Sie die oben gezeigte auswählen. Es gibt zwei Gründe:


Abhängigkeitsproblem


Aus der Dokumentation zu REST Assured geht hervor , dass zur Verwendung von Oauth1 oder Oauth2 (durch Angabe eines Tokens als Abfrageparameter) je nach Scribe Berechtigungen hinzugefügt werden müssen. Das Importieren der neuesten Version hilft Ihnen jedoch nicht weiter - es tritt ein Fehler auf , der in einem der offenen Probleme beschrieben wird . Sie können es nur lösen, indem Sie die alte Version der Bibliothek 2.5.3 importieren. In diesem Fall werden Sie jedoch auf ein anderes Problem stoßen.


Im Allgemeinen funktioniert keine andere Version von Scribe mit Oauth2 REST Assured Version 3.0.3 und höher (und die aktuelle Version 4.0.0 hat dies nicht behoben).


Die Protokollierung funktioniert nicht


Filter werden auf Abfragen in einer bestimmten Reihenfolge angewendet. Und AuthenticationScheme wird nach ihnen angewendet. Dies bedeutet, dass es schwierig sein wird, ein Problem mit der Autorisierung im Test zu erkennen - es wird nicht verpfändet.


Weitere Informationen zur REST Assured-Syntax


Eine große Anzahl von Tests bedeutet normalerweise, dass sie auch komplex sind. Und wenn die API das Hauptthema des Testens ist und Sie nicht nur die JSON-Felder, sondern auch die Geschäftslogik überprüfen müssen, wird der Test mit REST Assured zu einem Blatt:


 @Test public void shouldCorrectlyCountAddedCookies() { Integer addNumber = 10; JsonPath beforeCookies = given() .when() .get("/latestcookies") .then() .assertThat() .statusCode(200) .extract() .jsonPath(); String beforeId = beforeCookies.getString("id"); JsonPath afterCookies = given() .body(String.format("{number: %s}", addNumber)) .when() .put("/cookies") .then() .assertThat() .statusCode(200) .extract() .jsonPath(); Integer afterNumber = afterCookies.getInt("number"); String afterId = afterCookies.getString("id"); JsonPath history = given() .when() .get("/history") .then() .assertThat() .statusCode(200) .extract() .jsonPath(); assertThat(history.getInt(String.format("records.find{r -> r.id == %s}.number", beforeId))) .isEqualTo(afterNumber - addNumber); assertThat(history.getInt(String.format("records.find{r -> r.id == %s}.number", afterId))) .isEqualTo(afterNumber); } 

Dieser Test bestätigt, dass wir beim Füttern eines Monster-Cookies korrekt berechnen, wie viele Cookies ihm gegeben wurden, und dies in der Geschichte angeben. Auf den ersten Blick kann dies jedoch nicht verstanden werden - alle Anforderungen sehen gleich aus, und es ist nicht klar, wo die Vorbereitung der Daten über die API endet und wohin die Testanforderung gesendet wird.


given() , when() und then() REST Assured von BDD nimmt, wie Spock oder Cucumber. Bei komplexen Tests geht jedoch ihre Bedeutung verloren, da der Umfang des Tests viel größer als eine Anforderung wird - dies ist eine kleine Aktion, die durch eine Zeile angezeigt werden muss. Und dafür können Sie REST Assured-Anrufe an Hilfsklassen weiterleiten:


 public class CookieMonsterHelper { public static JsonPath getCookies() { return given() .when() .get("/cookiesformonster") .then() .extract() .jsonPath(); } ... } 

Und rufen Sie den Test an:


 JsonPath response = CookieMonsterHelper.getCookies(); 

Es ist gut, wenn solche Hilfsklassen universell sind, sodass ein Aufruf einer Methode in eine große Anzahl von Tests eingebettet werden kann. Dann können sie im Allgemeinen in eine separate Bibliothek gestellt werden: Plötzlich müssen Sie die Methode irgendwann in einem anderen Projekt aufrufen. Nur in diesem Fall müssen Sie alle Antwortprüfungen entfernen, die Rest Assured durchführen kann. Schließlich können häufig sehr unterschiedliche Daten als Antwort auf dieselbe Anforderung zurückgegeben werden.


Fazit


REST Assured ist eine Bibliothek zum Testen. Sie weiß, wie man zwei Dinge macht: Anfragen senden und Antworten überprüfen. Wenn wir versuchen, es aus den Tests zu entfernen und alle Überprüfungen zu entfernen, wird es zu einem HTTP-Client .


Wenn Sie eine große Anzahl von Tests schreiben und diese weiterhin unterstützen müssen, überlegen Sie, ob Sie einen HTTP-Client mit umständlicher Syntax, statischer Konfiguration, Verwirrung in der Reihenfolge der Anwendung von Filtern und Spezifikationen und einer Protokollierung benötigen, die leicht beschädigt werden kann. Vor vielleicht neun Jahren war REST Assured das bequemste Werkzeug, aber während dieser Zeit tauchten Alternativen auf - Nachrüstung, Feign, Unirest usw. -, die solche Funktionen nicht haben.


Die meisten der im Artikel beschriebenen Probleme manifestieren sich in großen Projekten. Wenn Sie schnell ein paar Tests schreiben und sie für immer vergessen müssen und Retrofit es nicht mag, ist REST Assured die beste Option.


Wenn Sie bereits Tests mit REST Assured schreiben, müssen Sie sich nicht beeilen, um alles neu zu schreiben. Wenn sie stabil und schnell sind, wird sie mehr Zeit in Anspruch nehmen als praktische Vorteile bringen. Wenn nicht, ist REST Assured nicht Ihr Hauptproblem.


Täglich wächst die Anzahl der in DINS für die RingCentral-API geschriebenen Tests, und sie verwenden weiterhin REST Assured. Der Zeitaufwand für den Wechsel zu einem anderen HTTP-Client, zumindest bei neuen Tests, ist zu groß, und die erstellten Hilfsklassen und Methoden, mit denen die Testkonfiguration konfiguriert wird, lösen die meisten Probleme. In diesem Fall ist es wichtiger, die Integrität des Projekts mit Tests aufrechtzuerhalten, als den schönsten und modischsten Kunden zu verwenden. REST Assured erledigt trotz seiner Mängel seine Hauptaufgabe.

Source: https://habr.com/ru/post/de464225/


All Articles