REST assuré: ce que nous avons appris de cinq ans d'utilisation de l'outil

REST assuré - DSL pour tester les services REST, qui est intégré aux tests Java. Cette solution est apparue il y a plus de neuf ans et est devenue populaire en raison de sa simplicité et de sa fonctionnalité pratique.


Dans DINS, nous avons écrit plus de 17000 tests avec lui et au cours des cinq années d'utilisation, nous avons rencontré de nombreux pièges qui ne peuvent pas être découverts juste après l'importation de la bibliothèque dans le projet: un contexte statique, une confusion dans l'ordre dans lequel les filtres sont appliqués à la requête, des difficultés à structurer le test.


Cet article concerne ces fonctionnalités implicites de REST Assured. Ils doivent être pris en compte s'il y a une chance que le nombre de tests dans le projet augmente rapidement - afin que vous n'ayez pas à les réécrire plus tard.


image


Que testons-nous


DINS participe au développement de la plateforme UCaaS. En particulier, nous développons et testons l'API que RingCentral utilise elle-même et fournit aux développeurs tiers .


Lors du développement d'une API, il est important de s'assurer qu'elle fonctionne correctement, mais lorsque vous la distribuez, vous devez vérifier beaucoup plus de cas. Par conséquent, des dizaines et des centaines de tests sont ajoutés à chaque nouveau point de terminaison. Les tests sont écrits en Java, TestNG est sélectionné comme cadre de test et REST Assured est utilisé pour les demandes d'API.


Quand REST Assured en bénéficiera


Si votre objectif n'est pas de tester minutieusement l'intégralité de l'API, la manière la plus simple de le faire est d'utiliser REST Assured. Il est bien adapté pour vérifier la structure de réponse, le PVD et les tests de fumée.


Voici à quoi ressemble un test simple, qui vérifiera que le point de terminaison donne le statut 200 OK lors de l'accès:


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

Les mots-clés given , when et then forment la demande: given détermine ce qui sera envoyé dans la demande, when –– avec quelle méthode et à quel noeud final nous envoyons la demande, then –– comment la réponse reçue est vérifiée. De plus, vous pouvez extraire le corps de la réponse sous la forme d'un objet de type JsonPath ou XmlPath , puis utiliser les données reçues.


Les vrais tests sont généralement plus gros et plus compliqués. Des en-têtes, cookies, autorisation, corps de requête sont ajoutés aux requêtes. Et si l'API testée ne se compose pas de dizaines de ressources uniques, chacune nécessitant des paramètres spéciaux, vous souhaiterez stocker des modèles prêts à l'emploi quelque part pour les ajouter plus tard à un appel spécifique du test.


Pour cela, dans REST Assured il y a:


  • RequestSpecification / ResponseSpecification ;
  • configuration de base;
  • filtres.

RequestSpecification et ResponseSpecification


Ces deux classes vous permettent de déterminer les paramètres de demande et les attentes de la réponse:


 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"); 

Une spécification est utilisée dans plusieurs appels, tests et classes de test, selon l'endroit où elle est définie - il n'y a aucune restriction. Vous pouvez même ajouter plusieurs spécifications à une seule demande. Cependant, c'est une source potentielle de problèmes :


 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); 

Journal des appels:


 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) 

Il s'est avéré que tous les en-têtes ont été ajoutés à l'appel, mais l'URI est soudainement devenu localhost - bien qu'il ait été ajouté dans la première spécification.


Cela est dû au fait que REST Assured gère les remplacements pour les paramètres de demande différemment (il en va de même pour la réponse). Des en-têtes ou des filtres sont ajoutés à la liste, puis appliqués à leur tour. Il ne peut y avoir qu'un seul URI, donc le dernier est appliqué. Elle n'était pas spécifiée dans la dernière spécification ajoutée - par conséquent, REST Assured la remplace par la valeur par défaut (localhost).


Si vous ajoutez une spécification à la demande, ajoutez-en une . Le conseil semble évident, mais lorsque le projet avec des tests se développe, des classes d'assistance et des classes de test de base apparaissent, des méthodes avant apparaissent à l'intérieur. Il devient difficile de suivre ce qui se passe réellement avec votre demande, surtout si plusieurs personnes écrivent des tests à la fois.


Configuration de base REST assurée


Un autre moyen de modèle de requêtes dans REST Assured consiste à configurer la configuration de base et à définir les champs statiques de la classe RestAssured:


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

Des valeurs seront automatiquement ajoutées à la demande à chaque fois. La configuration est combinée avec les annotations @BeforeMethod dans TestNG et @BeforeEach dans JUnit - vous pouvez donc être sûr que chaque test que vous exécutez commencera avec les mêmes paramètres.


Cependant, la configuration sera une source potentielle de problèmes, car elle est statique .


Exemple: avant chaque test, nous prenons un utilisateur de test, obtenons un jeton d'autorisation pour lui, puis l'ajoutons via AuthenticationScheme ou un filtre d'autorisation à la configuration de base. Tant que les tests s'exécuteront sur un seul thread, tout fonctionnera.
Lorsqu'il y a trop de tests, la décision habituelle de diviser leur exécution en plusieurs threads conduira à réécrire un morceau de code afin que le jeton d'un thread ne tombe pas dans le voisin.


Filtres assurés REST


Les filtres modifient les demandes avant l'envoi et les réponses avant de vérifier la conformité aux attentes spécifiées. Exemple d'application - ajout de journalisation ou d'autorisation:


 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()) ... 

Les filtres ajoutés à la demande sont stockés dans LinkedList . Avant de faire une demande, REST Assured la modifie en parcourant la liste et en appliquant un filtre après l'autre. Ensuite, la même chose se fait avec la réponse qui est venue.


L'ordre des filtres est important . Ces deux requêtes mèneront à des journaux différents: le premier indiquera l'en-tête d'autorisation, le second - non. Dans ce cas, l'en-tête sera ajouté aux deux demandes - juste dans le premier cas, REST Assured ajoutera d'abord l'autorisation avant de s'inscrire, et dans le second - vice versa.


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

En plus de la règle habituelle selon laquelle les filtres sont appliqués dans l'ordre dans lequel ils sont ajoutés, il est toujours possible de hiérarchiser votre filtre en implémentant l'interface OrderedFilter . Il vous permet de définir une priorité numérique spéciale pour le filtre, au-dessus ou en dessous de la valeur par défaut (1000). Les filtres avec une priorité supérieure seront exécutés plus tôt que d'habitude, avec une priorité inférieure - après eux.


Bien sûr, ici, vous pouvez être confus et définir accidentellement les deux filtres sur la même priorité, par exemple, à 999. Ensuite, celui qui a été ajouté avant sera appliqué à la demande en premier.


Pas seulement des filtres


La procédure d'autorisation via les filtres est indiquée ci-dessus. Mais en plus de cette méthode dans REST Assured, il y en a une autre, via AuthenticationScheme :


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

Il s'agit d'une méthode obsolète. Au lieu de cela, vous devez choisir celui illustré ci-dessus. Il y a deux raisons:


Problème de dépendance


La documentation de REST Assured indique que pour utiliser Oauth1 ou Oauth2 (en spécifiant un jeton comme paramètre de requête), des autorisations doivent être ajoutées en fonction du scribe. Cependant, l'importation de la dernière version ne vous aidera pas - vous rencontrerez une erreur décrite dans l'un des problèmes ouverts . Vous ne pouvez le résoudre qu'en important l'ancienne version de la bibliothèque, 2.5.3. Cependant, dans ce cas, vous rencontrerez un autre problème .


En général, aucune autre version de Scribe ne fonctionne avec Oauth2 REST Assured version 3.0.3 et supérieure (et la récente version 4.0.0 ne l'a pas corrigé).


La journalisation ne fonctionne pas


Les filtres sont appliqués aux requêtes dans un ordre spécifique. Et AuthenticationScheme est appliqué après eux. Cela signifie qu'il sera difficile de détecter un problème d'autorisation dans le test - il n'est pas promis.


En savoir plus sur la syntaxe REST Assured


Un grand nombre de tests signifie généralement qu'ils sont également complexes. Et si l'API est le principal sujet de test, et que vous devez vérifier non seulement les champs json, mais la logique métier, alors avec REST Assured, le test se transforme en feuille:


 @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); } 

Ce test vérifie que lorsque nous nourrissons un cookie monstre, nous calculons correctement le nombre de cookies qui lui ont été donnés et l'indiquons dans l'histoire. Mais à première vue, cela ne peut pas être compris - toutes les demandes se ressemblent et il n'est pas clair où se termine la préparation des données via l'API et où la demande de test est envoyée.


given() , when() then() REST Assured prend de BDD, comme Spock ou Cucumber. Cependant, dans les tests complexes, leur signification est perdue, car l'échelle du test devient beaucoup plus grande qu'une demande - il s'agit d'une petite action qui doit être indiquée par une ligne. Et pour cela, vous pouvez transférer les appels REST Assurés vers des classes auxiliaires:


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

Et appelez au test:


 JsonPath response = CookieMonsterHelper.getCookies(); 

C'est bien quand de telles classes auxiliaires sont universelles de sorte qu'un appel à une méthode peut être incorporé dans un grand nombre de tests - puis ils peuvent être placés dans une bibliothèque distincte en général: tout d'un coup, vous devez appeler la méthode à un moment donné dans un autre projet. Ce n'est que dans ce cas que vous devrez supprimer toute la vérification de la réponse que Rest Assured peut faire - après tout, des données très différentes peuvent souvent être retournées en réponse à la même demande.


Conclusion


REST Assured est une bibliothèque de test. Elle sait faire deux choses: envoyer des demandes et vérifier les réponses. Si nous essayons de le supprimer des tests et de supprimer toute validation, alors il se transforme en client HTTP .


Si vous devez écrire un grand nombre de tests et continuer à les prendre en charge, demandez-vous si vous avez besoin d'un client HTTP avec une syntaxe encombrante, une configuration statique, une confusion dans l'ordre d'application des filtres et des spécifications et une journalisation qui peut être facilement interrompue? Il y a peut-être neuf ans, REST Assured était l'outil le plus pratique, mais pendant ce temps, des alternatives sont apparues - Retrofit, Feign, Unirest, etc. - qui n'ont pas de telles fonctionnalités.


La plupart des problèmes décrits dans l'article se manifestent dans de grands projets. Si vous devez écrire rapidement quelques tests et les oublier pour toujours, et Retrofit ne l'aime pas, REST Assured est la meilleure option.


Si vous écrivez déjà des tests à l'aide de REST Assured, il n'est pas nécessaire de se précipiter pour tout réécrire. S'ils sont stables et rapides, cela passera plus de temps que cela n'apportera des avantages pratiques. Sinon, REST Assured n'est pas votre principal problème.


Chaque jour, le nombre de tests écrits en DINS pour l'API RingCentral augmente, et ils utilisent toujours REST Assured. Le temps qui devra être passé pour basculer vers un autre client HTTP, au moins dans les nouveaux tests, est trop important, et les classes et méthodes d'assistance créées qui configurent la configuration de test résolvent la plupart des problèmes. Dans ce cas, maintenir l'intégrité du projet avec des tests est plus important que d'utiliser le client le plus beau et le plus à la mode. REST Assured, malgré ses lacunes, fait son travail principal.

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


All Articles