REST Assured: o que aprendemos em cinco anos de uso da ferramenta

REST Assured - DSL para testar serviços REST, que é incorporado nos testes Java. Essa solução apareceu há mais de nove anos e se tornou popular devido à sua simplicidade e funcionalidade conveniente.


No DINS, escrevemos mais de 17.000 testes e, ao longo dos cinco anos de uso, encontramos muitas “armadilhas” que não podem ser encontradas logo após a importação da biblioteca para o projeto: um contexto estático, confusão na ordem em que os filtros são aplicados à consulta, dificuldades na estruturação do teste.


Este artigo é sobre esses recursos implícitos do REST Assured. Eles precisam ser levados em consideração se houver uma chance de o número de testes no projeto aumentar rapidamente - para que você não precise reescrevê-los mais tarde.


imagem


O que estamos testando


O DINS está envolvido no desenvolvimento da plataforma UCaaS. Em particular, desenvolvemos e testamos a API usada pelo RingCentral e que fornece a desenvolvedores de terceiros .


Ao desenvolver qualquer API, é importante garantir que ela funcione corretamente, mas quando você a distribuir, precisará verificar muito mais casos. Portanto, dezenas e centenas de testes são adicionados a cada novo terminal. Os testes são gravados em Java, o TestNG é selecionado como uma estrutura de teste e o REST Assured é usado para solicitações de API.


Quando o REST Assegurado será Beneficiado


Se seu objetivo não é testar completamente a API inteira, a maneira mais fácil de fazer isso é com o REST Assured. É adequado para verificar a estrutura de resposta, PVD e testes de fumaça.


É assim que um teste simples se parece, que verifica se o terminal fornece o status 200 OK ao acessá-lo:


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

As palavras-chave given , when e then formam a solicitação: given determina o que será enviado na solicitação, when –– com qual método e para qual terminal final enviamos a solicitação e then –– como a resposta recebida é verificada. Além disso, você pode extrair o corpo da resposta na forma de um objeto do tipo JsonPath ou XmlPath , em seguida, usar os dados recebidos.


Testes reais são geralmente maiores e mais complicados. Cabeçalhos, cookies, autorização, corpo da solicitação são adicionados às solicitações. E se a API em teste não consistir em dezenas de recursos exclusivos, cada um dos quais requer parâmetros especiais, convém armazenar modelos prontos em algum lugar para adicioná-los posteriormente a uma chamada específica no teste.


Para isso, no REST Assured existem:


  • RequestSpecification / ResponseSpecification ;
  • configuração básica;
  • filtros.

RequestSpecification e ResponseSpecification


Essas duas classes permitem determinar os parâmetros e expectativas de solicitação da resposta:


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

Uma especificação é usada em várias chamadas, testes e classes de teste, dependendo de onde está definida - não há restrições. Você pode até adicionar várias especificações a uma única solicitação. No entanto, esta é uma fonte potencial de problemas :


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

Registro de chamadas:


 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) 

Aconteceu que todos os cabeçalhos foram adicionados à chamada, mas o URI tornou-se subitamente localhost - embora tenha sido adicionado na primeira especificação.


Isso aconteceu devido ao fato de o REST Assured manipular substituições para parâmetros de solicitação de maneira diferente (o mesmo ocorre com a resposta). Cabeçalhos ou filtros são adicionados à lista e depois aplicados por sua vez. Pode haver apenas um URI, portanto o último é aplicado. Ele não foi especificado na última especificação adicionada - portanto, o REST Assured o substitui pelo valor padrão (localhost).


Se você adicionar uma especificação à solicitação, adicione uma . O conselho parece óbvio, mas quando o projeto com testes cresce, as classes auxiliares e as classes básicas de teste aparecem, os métodos anteriores aparecem dentro deles. Manter o controle do que realmente está acontecendo com sua solicitação se torna difícil, especialmente se várias pessoas escrevem testes ao mesmo tempo.


Configuração Assegurada REST Básica


Outra maneira de consultas de modelo no REST Assured é configurar a configuração básica e definir os campos estáticos da classe RestAssured:


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

Os valores serão adicionados automaticamente à solicitação a cada vez. A configuração é combinada com as anotações @BeforeMethod no TestNG e @BeforeEach no JUnit - para que você possa ter certeza de que todos os testes executados serão iniciados com os mesmos parâmetros.


No entanto, a configuração será uma fonte potencial de problemas, porque é estática .


Exemplo: antes de cada teste, pegamos um usuário de teste, obtemos um token de autorização para ele e o adicionamos através de AuthenticationScheme ou um filtro de autorização à configuração básica. Enquanto os testes forem executados em um único thread, tudo funcionará.
Quando há muitos testes, a decisão usual de dividir sua execução em vários threads levará à reescrita de um trecho de código, para que um token de um thread não caia no vizinho.


Filtros garantidos REST


Os filtros modificam as solicitações antes do envio e as respostas antes de verificar a conformidade com as expectativas especificadas. Exemplo de aplicativo - adicionando log ou autorização:


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

Os filtros adicionados à solicitação são armazenados no LinkedList . Antes de fazer uma solicitação, o REST Assured a modifica, percorrendo a lista e aplicando um filtro após o outro. Então a mesma coisa é feita com a resposta que veio.


A ordem dos filtros é importante . Essas duas consultas levarão a logs diferentes: a primeira indicará o cabeçalho da autorização, a segunda - não. Nesse caso, o cabeçalho será adicionado a ambas as solicitações - apenas no primeiro caso, o REST Assured primeiro adicionará autorização antes de se inscrever e, no segundo - vice-versa.


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

Além da regra usual de que os filtros são aplicados na ordem em que são adicionados, ainda há a oportunidade de priorizar seu filtro implementando a interface OrderedFilter . Permite definir uma prioridade numérica especial para o filtro, acima ou abaixo do padrão (1000). Os filtros com prioridade acima serão executados mais cedo do que o habitual, com prioridade abaixo - depois deles.


Obviamente, aqui você pode ficar confuso e definir acidentalmente os dois filtros para a mesma prioridade, por exemplo, em 999. Em seguida, o que foi adicionado antes será aplicado primeiro à solicitação.


Não apenas filtros


Como fazer a autorização através de filtros é mostrado acima. Mas além desse método no REST Assured, há outro, através do AuthenticationScheme :


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

Este é um método obsoleto. Em vez disso, você deve escolher o mostrado acima. Existem dois motivos:


Problema de Dependência


A documentação do REST Assured indica que, para usar Oauth1 ou Oauth2 (especificando um token como parâmetro de consulta), as autorizações devem ser adicionadas, dependendo do Scribe. No entanto, importar a versão mais recente não o ajudará - você encontrará um erro descrito em um dos problemas em aberto . Você pode resolvê-lo apenas importando a versão antiga da biblioteca, 2.5.3. No entanto, neste caso, você encontrará outro problema .


Em geral, nenhuma outra versão do Scribe funciona com o Oauth2 REST Assured versão 3.0.3 e superior (e a versão recente 4.0.0 não corrigiu isso).


O registro não funciona


Os filtros são aplicados às consultas em uma ordem específica. E o AuthenticationScheme é aplicado depois deles. Isso significa que será difícil detectar um problema com autorização no teste - ele não está comprometido.


Mais sobre sintaxe REST assegurada


Um grande número de testes geralmente significa que eles também são complexos. E se a API é o principal assunto do teste, e você precisa verificar não apenas os campos json, mas também a lógica de negócios, com REST Assured o teste se transforma em uma planilha:


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

Este teste verifica se, quando alimentamos um cookie monstro, calculamos corretamente quantos cookies foram dados a ele e o indicamos na história. Mas, à primeira vista, isso não pode ser entendido - todas as solicitações parecem iguais e não está claro onde termina a preparação dos dados por meio da API e para onde a solicitação de teste é enviada.


given() , when() e then() REST Assured recebe do BDD, como Spock ou Cucumber. No entanto, em testes complexos, seu significado é perdido, porque a escala do teste se torna muito maior que uma solicitação - essa é uma pequena ação que precisa ser indicada por uma linha. E para isso, você pode transferir chamadas REST Assured para classes auxiliares:


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

E ligue no teste:


 JsonPath response = CookieMonsterHelper.getCookies(); 

É bom que essas classes auxiliares sejam universais para que uma chamada para um método possa ser incorporada em um grande número de testes - eles podem ser colocados em uma biblioteca separada em geral: de repente, você precisa chamar o método em algum momento de outro projeto. Somente nesse caso, você terá que remover toda a verificação da resposta que o Rest Assured pode fazer - afinal, dados muito diferentes podem ser retornados com frequência em resposta à mesma solicitação.


Conclusão


O REST Assured é uma biblioteca para teste. Ela sabe como fazer duas coisas: enviar solicitações e verificar respostas. Se tentarmos removê-lo dos testes e remover toda a validação, ele se transformará em um cliente HTTP .


Se você precisar escrever um grande número de testes e continuar a apoiá-los, pense se você precisa de um cliente HTTP com sintaxe pesada, configuração estática, confusão na ordem de aplicação de filtros e especificações e log que pode ser facilmente quebrado. Talvez há nove anos, o REST Assured foi a ferramenta mais conveniente, mas durante esse período surgiram alternativas - Retrofit, Feign, Unirest etc. - que não possuem esses recursos.


A maioria dos problemas descritos no artigo se manifesta em grandes projetos. Se você precisar escrever rapidamente alguns testes e esquecê-los para sempre, e o Retrofit não gostar, o REST Assured é a melhor opção.


Se você já está escrevendo testes usando o REST Assured, não é necessário se apressar para reescrever tudo. Se eles são estáveis ​​e rápidos, gastará mais do seu tempo do que trará benefícios práticos. Caso contrário, o REST Assured não é o seu principal problema.


Todos os dias, o número de testes escritos em DINS para a API RingCentral está aumentando e eles ainda usam o REST Assured. A quantidade de tempo que será gasta para mudar para outro cliente HTTP, pelo menos em novos testes, é muito grande e as classes e métodos auxiliares criados que configuram a configuração de teste resolvem a maioria dos problemas. Nesse caso, manter a integridade do projeto com testes é mais importante do que usar o cliente mais bonito e elegante. REST Assured, apesar de suas deficiências, faz seu trabalho principal.

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


All Articles