Com a adoção de microserviços como arquitetura para distribuir aplicações de maneira escalável, separada por responsabilidade e de maneira a entregar somente o que se propõe a aplicação de testes para esta nova camada/arquitetura se faz necessário.
Uma arquitetura baseada em microserviços é disponibilizada através de serviços (APIs) em um back-end. Estes serviços são consumidos por diferentes front-ends (web, mobile, desktop…), existindo o que chamamos de API gateway para transportar somente os dados necessários para o front-end.
É comum que os desenvolvedores criem testes unitários e de integração para suas APIs. Também é comum QAs criarem testes no front-end web ou mobile. Mas em um projeto de microserviços precisamos também testar as chamadas (requisições) e suas respostas, o que chamamos na figura abaixo de testes de api de consumo. Se não é feita a correta validação nesta camada problemas no front-end podem aparecer.
Diferente camadas de uma aplicação
Pipeline para Automação de Testes de API
Pipeline é a aplicação do processo de automação nas etapas do desenvolvimento das aplicações. Os exemplos mais clássicos são as pipelines de Continuous Integration (CI) e Continuous Delivery (CD). De modo geral é automatizar desde a compilação, execução de testes, empacotamento da aplicação e, em alguns casos, a entrega. Em um processo simples de desenvolvimento fim-a-fim teríamos a pipeline abaixo:
compilação (build) -> testes unitários -> testes funcionais -> geração da aplicação para entrega
Pipeline de automação de teste
O desenvolvimento de scripts de teste é um desenvolvimento de software. A diferença está no foco: nós estamos validando se a aplicação foi construída de acordos com os requisitos ao invés de construir a aplicação de acordo com os requisitos.
Quando criamos uma pipeline de automação de teste aplicamos os tipos de teste, que irão variar de acordo com o foco que você deseja.
Quando falamos sobre testes de API, recomendamos a seguinte pipeline:
Pipeline de Automação de uma API
Como o pipeline é uma sequência, nós recomendamos a ordem acima porque:
primeiro queremos saber se todos os serviços estão disponíveis (health check)
depois queremos saber se todos os serviços possuem os contratos esperados (contrato)
em seguida queremos saber se as jornadas dos usuários / cenários de uso não apresentam problemas (aceitação)
no fim os testes funcionais para garantir os principais caminhos e os fluxos de exceção
O que é REST-Assured
Rest-Assured é uma biblioteca Java, disponibilizada através de um DSL, para testar APIs RESTful.
Com ele podemos manipular todas as informações necessárias para criar requisições e validá-las (teste), como:
manipular headers e cookies
criar requisições através dos principais métodos HTTP
enviar parâmetros para requisições
obter o retorno das requisições
validar todo o tipo de dados de retorno das requisições
Para que possamos automatizar uma API REST é necessário inserir três bibliotecas nas dependências no projeto:
biblioteca do Rest-Assured
biblioteca do JSON Schema Validation
biblioteca de um framework de teste unitário (JUnit 5)
Para criar os scripts de teste automatizado utilizaremos o JUnit 5 como framework de testes unitários e suporte à criação de testes e Maven como framework de build e gerenciamento de dependências.
Você pode ou utilizar o Projeto de Teste para o desenvolvimento dos exercícios ou criar o seu próprio projeto.
Utilizando o Projeto de Teste
Eu recomendo a você fazer o clone ou download do projeto no GitHub, assim você já terá todas as dependências necessárias.
Para efetuar o clone do projeto execute o seguinte comando no seu Terminal / Prompt de Comando:
4. Importe o projeto utilizando a sua IDE favorita.
Estrutura da classe de teste
Sempre que criarmos um script de teste para uma API REST com o Rest-Assured devemos adicionar os imports necessários para a utilização do Rest-Assured e para a validação dos resultados esperados do teste.
Abaixo temos um exemplo de uma classe inicial para os scripts de teste automatizado para uma API REST.
No comentário //imports, além dos imports para o teste e pré-condição (os dois primeiros) temos dois imports muito importantes. Eles são responsáveis por usos específicos de funcionalidades. Entenda-os através dos comentários no final de cada linha do código abaixo:
1 // import de todos os possíveis métodos de utilização no Rest-Assured2 import staticio.restassured.RestAssured.*;3 4 // import dos possíveis métodos de validação dos resultados esperados5 import staticorg.hamcrest.CoreMatchers.*;
Teste
O método de teste que irá executar as ações necessárias na API, a fim de validá-la.
1 // import da anotação @Test do JUnit 52 @Test3 voidexemploDeTeste(){4 }
Sobre a abordagem de criação de teste adotada neste livro
No geral podemos utilizar duas abordagens: uma classe de teste por teste ou diversos testes em uma classe de teste.
A abordagem uma classe de teste por teste tem o intuito de criar uma classe e apenas um método de teste dentro desta classe. Cada novo teste deve ser uma nova classe.
1 classUmaClasseDeTestePorTeste{2 3 // você terá apenas um único teste na classe4 @Test5 voidunicoTesteNaClasse(){6 }7 8 }
Benefício
Maior controle sobre os diferentes testes criados
Desvantagens
Maior manutenção
Maior gestão de pré e pós condições
Maior quantidade de classes de teste
A abordagem diversos testes em uma classe de teste tem o intuito de criar uma classe de teste e ter diversos métodos de teste dentro dela.
Centralização de testes para uma mesma funcionalidade
Desvantagens
Maior gestão na criação de suites de teste
Testes Funcionais para uma API REST com Rest-Assured
O Rest-Assured trabalha com uma estrutura de métodos chamada de DSL - Domain Specific Language que visa facilitar a criação de testes usando métodos que expressam o que queremos fazer no script de teste.
Quando utilizamos uma DSL podemos encadear a utilização de métodos sempre inserindo um ponto . seguido pelos próximos métodos que desejarmos utilizar ao invés de inserir cada método em uma nova linha. Esta abordagem usa o design pattern Fluent Builder para facilitar a utilização de vários métodos
A estrutura básica dos comandos do Rest-Assured se dá por:
Método
Descrição
given()
Pré-condições de uma requisição para a API. Não é obrigatório
when()
Ações que a API deve executar. Leva obrigatoriamente o método HTTP
then()
Resultados esperados da requisição. Inserimos as validações dos resultados obtidos os extraimos o resultado para uso
Comandos básicos
Given
Sempre utilizado através do método RestAssured.given() ou given() quando possuímos o import estático import static io.restassured.RestAssured.*;
É a pré-condição da requisição. Existem alguns tipos de pré-condições que podem ser necessárias em uma requisição, como:
autenticação
parâmetros
cookies
headers
configurações do endpoint
content-type
body
When
Sempre utilizado através do método RestAssured.when() ou when() quando possuimos o import estático import static io.restassured.RestAssured.*;
Ele é obrigatório porque, praticamente, inicia a requisição. Devemos informar qual o verbo HTTP utilizaremos para efetuar a requisição. Existem diversos métodos HTTP, porém no geral utilizaremos:
Metodo HTTP
Exemplo
GET
when().get()
POST
when().post()
PUT
when().put()
DELETE
when().delete()
Parâmetros nos verbos HTTP
Normalmente passamos a URL completa como parâmetro para o método HTTP. Ela é composta pela URL inicial do projeto mais o path definido no endpoint. Exemplo: o primeiro endpoint listado em simulacoes é o GET /api/v1/simulacoes. Ele é o seu path.
URL inicial: http://localhost:8088
path: /api/v1/simulacoes
Para que possamos utilizar o método HTTP GET no endpoint the simulacoes devemos passar a URL inicial + path. Veja o exemplo de código:
Note que cada endpoint pode ter um endereço diferente que difere apenas no que vem depois do nome do recurso, que neste caso é simulacoes.
Then
Sempre utilizado através do método then() após utilizarmos qualquer método HTTP utilizado no método when().
É associado aos resultados esperados de uma requisição ou extração de dados. Nenhum dos métodos de validação é obrigatório, mas o uso de alguns são altamente recomendados.
São métodos disponíveis para o then:
Método
Descrição
statusCode
valida o status code do retorno
body
valida dos dados de retorno da body
contentType
valida o content-type de retorno
header
valida o header retornado
cookie
valida alguma informação em cookies
Exemplo completo de script
Apenas para você ter uma base, este seria um script completo para uma requisição GET.
A primeira coisa que temos que analisar, não somente em uma requisição do tipo GET, mas em todos os tipos (métodos HTTP) é se teremos alguma pré-condição. Se não houver uma pré-condição o início do script sempre usará função when().
Para efetuar uma requisição GET utilize o método get(). Não esqueça que devemos sempre passar por parâmetro o endpoint da requisição.
No Exemplo completo do script podemos ver uma requisição GET para o endpoint simulacoes na linha 19.
Como validar os resultados esperados
Um script de teste automatizado só pode ser chamado assim se este conter formas de validar os resultados esperados de sua execução. Para que possamos validar os resultados esperados será necessário usar o método then() logo após a chamada do método HTTP.
1 when().2 get("http://localhost:8088/api/v1/simulacoes").3 then().4 // validações dos resultados
É uma boa prática validar, pelo menos, dois itens do retorno da requisição:
status code: código de status retornado pela requisição
No statusCode inserimos como parâmetro o código esperado pelo retorno do resultado.
No body inserimos a validação, que será explicada abaixo.
Body como unico objeto
O método body refere-se aos dados de retorno da requisição, que geralmente está no formato JSON. Digamos que tenhamos o seguinte retorno da body de uma requisição do tipo GET:
1 {2 "nome":"João",3 "idade":274 }
O método body do Rest-Assured espera receber dois parâmetros:
atributo da body
tipo de validação
O atributo da body é o atributo que é apresentado no retorno de dados da body e deve estar sempre entre aspas duplas " ". No exemplo que temos seria o nome ou idade.
O tipo de validação é a forma que o valor retornado será validado. Para que o script possa fazer essa validação utilizamos métodos do Hamcrest chamados de matchers.
Na grande maioria dos casos utilizamos ou o matcher is() ou o matcher equalTo(). Semanticamente os dois fazem a mesma coisa: validam se um resultado é ou é igual a. Para ambos precisamos inserir como parâmetro o valor esperado.
O script abaixo mostra um exemplo da validação de dados.
Quando este tipo de situação ocorrer o primeiro parâmetro da body, que é o atributo da body, deve conter o índice do array + valor do atributo no seguinte formato:
[indice].atributo
O exemplo abaixo demonstra como inserir o índice para validar os atributos. No lado esquerdo há dois objetos como retorno de um array. Como qualquer contagem de um array começa em zero (0), a primeira posição do array é zero. Note que, no lado direito, há as validações para cada um dos atributos utilizando o índice zero do array [0]. Para a segunda posição o índice do array é um (1), logo no segundo conjunto de validações é utilizado o índice um do array [1].
Explicação sobre validação de dados em um array
O script abaixo mostra um exemplo da validação de dados para um retorno do tipo array.
Para as validações da body lembre-se que o primeiro parâmetro é o atributo. Porém devemos iniciar com o índice do array, pois o retorno de dados é um array. Exemplo:
1 body("[0].cpf",equalTo("66414919004")
Observação 3
Para a validação do atributo valor, adicione um “efe” (f) ao final do número. Devemos inseri-lo para informar ao script que o tipo de dado é um número de ponto flutuante (float). Se você não inserir esta letra a validação vai falhar, mesmo o resultado da falha apresentando valores idênticos para resultado esperado e resultado obtido.
Resultados Esperados
Script executado com sucesso (sem nenhum erro de validação ou qualquer outro)
Efetuando uma requisição GET com parâmetros de path
Existem diferentes tipos de parâmetros para uma API REST. Um dos tipos é chamado de path parameter ou parâmetro de path.
Como o próprio nome já diz o parâmetro é inserido no path, ou seja, na própria URL. Este é o caso, por exemplo, da requisição do tipo GET no endpoint Restrições.
Um path parameter é identificado por um nome entre chaves {nome}. Geralmente este nome é o atributo que devemos informar na URL. Exemplo:
GET /api/v1/restricoes/{cpf}
Note que, na URL de acesso para o GET em restricoes há no final um path parameter chamado CPF. Podemos inferir que, para consultar uma restrição, devemos informar um CPF no lugar de {cpf}. Sempre consulte a documentação da API para ter certeza qual parâmetro deve ser utilizado.
Você já aprendeu a utilizá-lo de forma manual nos exercícios com o Postman. Agora aprenderemos como fazer isso no Rest-Assured.
Há duas formas de inserir o path parameter no RestAssured:
unnamed parameter
named parameter
Unnamed parameter (parâmetro não nomeado)
No parâmetro não nomeado inserimos a URL completa (a mesma da documentação) e adicionamos um parâmetro a mais no método HTTP para informar qual o seu valor.
Note que no exemplo acima a URL, segundo a documentação, permanece a mesma (com o path parameter) e o valor é inserido como um parâmetro a mais no método get. Isso serve para qualquer método. O Rest-Assured faré a substituição do {cpf} pelo valor "66414919004" dinamicamente.
Você pode estar pensando: não seria melhor informar diretamente o CPF para o método get()? Se você fosse fazer desta maneira, teríamos:
Isso parece mais fácil mas vai fazer com que você tenham uma maior manutenção ao decorrer do tempo. Utilizando o método proposto será mais fácil de, futuramente, usar variáveis para que o valor seja automaticamente substituido. Logo prefira sempre o método proposto.
Se existir mais de um path parameter inserimos parâmetros adicionais no método para informar o valor.
No exemplo acima existem dois path parameters. Para informar o valor temos que seguir a mesma lógica de índice. O primeiro parâmetro {codigoHotel} é o primeiro valor 10738 e o segundo parâmetro {numeroQuarto} é o segundo valor 215.
Neste exemplo o Rest-Asssured fará a substituição dos valores, efetuando a seguinte requisição: GET /reservas/10738/215
Named parameter (parâmetro nomeado)
No parâmetro nomeado os parâmetros e valores são descritos primeiro como uma pré-condição através do método pathParam() para o método given(). Após inseri-los não será necessário adicionar mais parâmetros no método HTTP para o método when().get().
No exemplo acima primeiro descrevemos quais são os nomes e valores dos path parameters. Quando efetuamos o get() para a URL não é mais necessário adicionar valor dos parâmetros. O Rest-Assured se encarregará de fazer a associação dos nomes dos parâmetros e substitui-los pelos valores.
Efetuando uma requisição GET com parâmetros de query
O segundo tipo de parâmetros para um API REST é o query parameter.
Como o próprio nome já diz, ele serve para efetuarmos pesquisas em atributos presentes na requisição. O parâmetro, que é informado diretamente na URL, tem a seguinte sintaxe:
1 GET /api/v1/simulacoes?nome=Fulano
Para utilizá-lo, devemos:
iniciar com um ponto de interrogação (?) que é o caractere que indica pesquisa, que deve ser inserido após o nome do endpoint
seguido pelo nome do atributo que será utilizado
ter um sinal de igual (=)
e finalizar com o valor que desejamos pesquisar
No exemplo acima efetuamos uma requisição em simulacoes efetuando uma pesquisa no atributo nome com o valor Fulano.
Como sei que o atributo é do tipo query parameter?
Se você olhar na documentação do Swagger o endpoint selecionado mostrará se existe um parâmetro para ser inserido e qual o seu tipo. No endpointGET /api/v1/simulacoes em Simulações há um parâmetro chamado nome que é do tipo query, logo ele é um query parameter.
Localização do parâmetro de query
Como usar o query parameter
Na utilização do given() existe o método subsequente queryParameter. Ele é o responsável por informar ao Rest-Assured que desejamos efetuar uma pesquisa. Este método recebe dois parâmetros:
No exemplo acima estamos informando um query parameter com nome de atributo nome e valor de atributo Fulano. O Rest-Assured identificará o método e fará a inclusão da sintaxe ?atributo=valor automaticamente na URL.
Você poderia fazer o mesmo diretamente na URL como no exemplo abaixo.
Esta forma não é recomendada porque você perderá a legibilidade de facilmente enxergar que há um query parameter e a fácil manutenção na troca do valor da pesquisa, onde o valor do atributo nome está localizado diretamente na URL da requisição.
Criação de um BaseTest
Sempre há situações em um projeto de automação de teste onde existem pré e pós condições para os scripts de teste automatizados. A prática normal é adicioná-los em cada script de teste, porém isso gera uma duplicidade de código e traz uma maior manutenção.
BaseTest (teste base) é o nome dado a uma boa prática adotada na escrita de testes automatizados com o intuito de colocar, em um único local, todas as execuções comuns de determinados scripts de teste.
Comumente ela é associada a ações de pré e pós condições.
São exemplos de ações que podem ser inseridas em um BaseTest:
criação de logs
login/logout da aplicação
definição de URL comuns aos testes
abertura e fechamento de servidores
abertura e fechamento de conexões com um banco de dados
inicialização de atributos e/ou objetos globais na utilização do teste
Como o Rest-Assured gerencia a URL
Você notou que estamos passando a URL completa para efetuar as requisições nos endpoints? Normalmente nós efetuamos uma separação da URL para que possamos futuramente alterá-la. Imagine você executando os mesmos testes em diferentes ambientes. Quase toda a URL pode mudar.
Uma explicação básica sobre o RestAssured: ele assume, se você não informar a base da URL até a porta, que você está usando o localhost na porta 8080. Por examplo, se você estivesse realmente executando estes exercícios usando esta mesma URL inicial padrão do Rest-Assured você poderia ter a chamada da API assim:
1 when().2 get("/api/v1/simulacoes")
Note que o exemplo acima só possui o path do endpoint. Mas como estamos executando em uma porta diferente devemos informar a URL. E estamos fazendo isso com a URL completa:
O ideal é efetuarmos a separação da URL completa para facilitar futuras manutenções e apontamento de ambientes. Para isso o Rest-Assured provê de três atributos:
baseURI
port
basePath
Todos eles são atributos da classe RestAssured.
Cada atributo tem uma responsabilidade sobre a URL completa, sendo:
baseURI: responsável pela URL inicial
port: responsável pela porta
basePath: responsável pelo path até o nome do recurso
Atributo
Valor
chamada GET
http://localhost:8088/api/v1/simulacoes
baseURI
http://localhost
port
8088
basePath
/api/v1
recurso
/simulacoes
O recurso é sempre adicionado no método HTTP como boa prática, por isso o RestAssured não dispôe de um atributo para ele.
Com isso nós conseguiríamos fazer o seguinte nos nossos scripts de teste:
Note que definimos todo a base da URL completa, restando apenas o uso do recurso /simulacoes no get(). Isso nos traz a possibilidade de evitarmos a duplidade de código. Usaremos esta abordagem nos scripts através do BaseTest.
Como criar um BaseTest
Um BaseTest tem alguns padrões que devem ser seguidos para a sua criação e utilização:
Deve ser uma classe abstrata
Pode possuir métodos de pré e/ou pós condições de diferentes tipos
Atributos que serão utilizados nos testes não devem ser privados (private) ou públicos (public), devem ser protegidos (protected)
Pode ser nomeado com o sufixo Base como boa prática
1 publicabstractclassApiBase{2 }
Agora podemos inserir uma pré condição na classe ApiBase para que esta seja compartilhada com todos os testes. O JUnit 5 possui dois tipos de pré condição:
@BeforeAll: demostra que o trecho de código deve executar antes de todos os testes (apenas uma vez)
@BeforeEach: demostra que o trecho de código inserido deve executar antes de cada teste
Nós não precisamos que o baseURI, port e basePath seja colocado em cada teste, certo? Podemos “adicioná-los” apenas uma vez para que todos os testes possam usar a mesma informação. Logo utilizademos o @BeforeAll. O método que criaremos para a pré condição, neste caso, deve ser estático, ou seja, possuir a keyword static antes do nome do método.
Uma vez criado o BaseTest devemos associá-lo ao script de teste automatizado através de uma herança (extensão). Para isso, em Java, utilizamos a keyword extends logo após o nome da classe do script de teste automatizado.
Antes do teste teste1() ser executado o método preCondicao() da classe ApiBase será executada porque este possui a anotação @BeforeAll. Logo a baseURI, port e basePath já será adicionados. Utilizamos apenas o recurso no método get(). Quando o teste teste2() executar os atributos já estarão com os dados necessários para efetuar a requisição. Como a pré condição que inserimos executa apenas uma vez para todos os testes não há a necessidade de executá-la novamente.
Efetuando uma requisição POST
Já sabemos que uma requisição do tipo POST envia uma informação para criar um registro, na maioria dos casos. O Rest-Assured utiliza o método post() para isso. Ele deve ser usando sempre depois do método then().
Como o intuito do POST é enviar dados, podemos fazê-lo de duas formas:
através de form parameters
através de body parameters
Logo é sempre necessário informar, via método given(), o tipo de envio de dados.
1 @Test2 publicvoidexemploPOST(){3 given().4 param("nomeDoParametro","valorDoParametro").// form parameter5 body(objeto).// body parameter6 when().7 post("/simulacoes");8 }
Nunca será necessário usar o form parameter em conjunto com o body parameter. O código acima é apenas um exemplo para ilustrar a utilização dos métodos. Para o form parameter tanto o método param quando o método formParam funcionam. Infelizmente não há exemplos neste livro sobre form parameter
O caso mais comum do uso do método POST é informar os dados através do request body (corpo ou conteúdo do request). Ele é utilizado através do método body() após o uso do método given().
Antes de tudo é importante informar o tipo de dados que estamos enviando para a body. Para isso utilizamos o método contentType(), informando como parâmetro o tipo de dados.
A documentação de Simulações POST /api/v1/simulacoes mostra que esta requisição necessita como “campo obrigatório” um objeto simulacao como request body que é apresentado pelo nome body abaixo do nome do atributo/objeto.
Imagem de como associar as informações necessárias
o atributo/objeto simulacao é obrigatório, e o tipo de parâmetro é um body (request body)
será necessário enviar os atributos descritos no exemplo
o tipo de dados que deve ser enviado é application/json
Note que o objeto simulacao é um objeto JSON.
Informando um objeto JSON: a forma mais simples
Há sempre a forma mais simples e a melhor forma. Nos próximos tópicos aprenderemos a melhor forma, mas por enquanto a forma mais simples nos dará a visão completa de como efetuar um POST com o Rest-Assured. Precisamos enviar os seguintes dados:
cpf
nome
email
valor
parcelas
seguro
Como os dados devem ser enviados pelo body estes dados devem ser enviados através de um objeto. A forma mais simples é a criação de um objeto JSONObject.
JSONObject
Esta class é pertencente a biblioteca org.json que já está no pom.xml do projeto. Ela é uma coleção de chave-valor (key-value) muito semelhante a um Map em Java.
No caso do JSONObject a chave (primeiro parâmetro) é o nome do atributo e o valor (segundo parâmetro) é o valor do atributo.
Sempre que houver a necessidade de efetuar um POST com um body devemos seguir estes passos:
Criar o objeto de dados para submissão (neste momento o JSONObject)
Informar como pré-condição (given()):
o content type do objeto (contentType())
a body contendo o objeto como parâmetro, sendo enviado como toString()
O toString() é necessário para transformar os dados informados no JSONObject em um objeto JSON.
1 // objeto que será enviado 2 JSONObjectdados=newJSONObject(); 3 dados.put("nome","Deltrano"); 4 dados.put("cpf","12345678930"); 5 dados.put("email","teste@gmail.com"); 6 dados.put("valor",22000); 7 dados.put("parcelas",2); 8 dados.put("seguro",true); 9 10 given().11 contentType("application/json").// informando o content-type do objeto12 body(dados.toString())// informando o objeto que será enviado
No exemplo acima primeiro criamos um objeto JSONObject com todos os dados necessários que estão no item 2 descrito anteriormente sobre a documentação da API. Após, na pré-condição given() informamos qual o contentType descrito no item 3 da documentação da API, que é application/json. Por último informamos o objeto JSONObject no body() que é o local onde passamos as informações para serem registradas.
———–> REVISAR <———–
Criação de um Builder
Builder é o nome dado a um padrão de projeto que tem o intuito de facilitar a criação de objetos concretos, que possuem uma lógica de criação complicada ou que tenham diversos atributos. Assim poderemos criar objetos de uma forma fluente, simplificada, intuitiva e independente.
Antes do builder
Sem a utilização do padrão Builder conseguimos criar objetos de duas formas:
Instância do objeto inicializando os dados pelo construtor
Instância do objeto inicializando os dados via setter
1 // instância do objeto inicializando os dados pelo construtor 2 Simulacaosimulacao=newSimulacao("Deltrano","12345678931","teste@gmail.com",22\ 3 000,2,true); 4 5 // instância do objeto inicializando os dados via setter 6 Simulacaosimulacao=newSimulacao(); 7 simulacao.setNome("Deltrano"); 8 simulacao.setCpf("12345678931"); 9 simulacao.setEmail("teste@gmail.com");10 simulacao.setValor(22000);11 simulacao.setParcela(2);12 simulacao.setSeguro(true);
A instância do objeto inicializando os dados pelo construtor não nos deixa claro quais são os atributos que devemos informar, dificultando a leitura e entendimento do código.
A instância do objeto inicializando os dados via setter é verbosa, ou seja, ha muitos comandos para que o objeto seja criado.
Ambas as abordagens funcionam muito bem, mas há formas de criar objetos de uma maneira mais rápida e de fácil entendimento do código quando visualizado.
Criação de Builders
Uma das funções do builder é ter uma melhor forma de criar os objetos sem ser pelo construtor da classe. Ele segue o seguinte padrão:
há os mesmos atributos para guardar os dados temporariamente
os métodos de informação de dados retornam a própria classe
existirá um método chamado build que chamará o construtor da classe e retornará uma nova instância
Para ter uma maior agilidade na criação de código, utilizaremos uma biblioteca que criará os métodos de getter, setter, o construtor e o builder.
Lombok
É um projeto que tem o intuito de diminuir a verbosidade das classes Java (Beans, DTOs, etc…). A vantagem é evitar a criação de código que sempre estará presente, ou seja, com poucos comandos é possível gerar os métodos de getter, setter, criar construtores, builders, entre outras coisas sem precisar escrever muito código.
O projeto já possui a biblioteca no arquivo pom.xml e você, nas pré-condições, já instalou o plugin e configurou a opção Annotation Processor no IntelliJ, logo nenhuma configuração é necessária neste momento.
O Lombok possui muitas anotações para facilitar a nossa vida, porém necessitamos de apenas duas anotações:
@Data: gera os getters e setters automaticamente para os atributos da classe
@Builder: gera o builder com os atributos para a classe
@AllArgsConstructor: gera um construtor com um parâmetro para cada atributo contido na classe
@RequiredArgsConstructor: mesmo que o @AllArgsConstructor, porém considerando apenas atributos que não são estáticos ou final
Um exemplo de utilização para um objeto fictício de cadastro seria:
A anotação @Builder adicione um método dentro da classe Cadastro, de forma dinâmica, chamada builder(). Este método é o responsável por habilitar o padrão builder para utilização.
Sendo assim, para criar um objeto Cadastro com dados, utilizamos o método builder() seguido pelos atributos que desejamos informar para criar o objeto. Isso é feito de forma encadeada, ou seja, apenas inserindo um ponto (.) seguido pelo atributo e assim por diante.
Note que é sempre necessário colocar, como último método, build(). Ele é quem fará a criação do objeto.
Existe sempre um padrão para utilização do builder:
inicia com o nome da classe + método builder()
seguido dos métodos do builder para informar os dados necessários
terminando com o método build()
Efetuando uma requisição PUT
O método PUT altera informações de um registro já existente. Normalmente um parâmetro, que é um identificador único do registro, é informado como path parameter.
Exemplo: PUT /v1/api/simulacaoes/{cpf}
É necessário verificar na documentação da API quais parâmetros são necessário informar para que a alteração dos dados seja executada. O Rest-Assured utiliza o método put() que deve ser utilizado depois do método then().
Várias implementações de desenvolvimento divergem entre sobre a atualização de registros. Algumas implementações analisam quais dados foram enviados e atualizam somente este, outras necessitam do objeto completo para atualizar os registros.
Na documentação do método PUT para Simulações note que é necessário enviar como parâmetro de body todo o objeto de Simulação.
put_swagger
Como proceder na atualização de dados
Dificilmente temos certezas do tipo de implementação sobre a atualização de registros. Logo adote sempre a abordagem de enviar todos os dados, mesmo que sejam os mesmos e não devam sofrer alterações.
Exemplo: se você apenas desejar alterar o atributo nome no registro, deverá enviar, além do nome alterado, os valores originais.
É recomendado que você utilize o padrão builder para construir o objeto com os dados.
No objeto Simulacao apenas o nome e email estão sendo alterados, porém todos os outros dados estão presentes na criação do objeto.
Note também que na pré-condição da requisição given() há um path parameter informando qual a chave de pesquisa do objeto (no caso o CPF) e também o objeto da simulação como parâmetro de body. Você já sabe, mas não custa lembrar: quando usamos um pathParam devemos informar o nome deste parâmetro entre chaves no envio da requisição.
Como alterar apenas os dados necessários para o PUT
Você deve ter notado no Exercício: criar uma requisição PUT que é necessário informar todos os dados do objeto simulação, via builder, com os dados idênticos aos já existentes quando não há necessidade de alterá-los.
Uma alternativa utilizando o próprio Rest-Assured é efetuar uma requisição de consulta, para que seja retornado o objeto simulação pela requisição, e associar o retorno do response body ao objeto. Mas como fazer isso?
Extraindo dados do response body
Há maneiras de extrair ou todo o response body ou somente alguma informação que necessitamos. Independente da maneira, após o método then() usaremos o método extract(). Este método, e os subsequentes que serão utilizados sempre retornarão alguma coisa.
Se houver a necessidade de extrair apenas um dos atributos presentes no response body, por exemplo, o id usaremos o método path() logo após o método extract(). Este método (o path()) necessita de um parâmetro que é o caminho dentro do response body para retornar. Neste caso o caminho refere-se ao atributo.
Exemplo: desejamos retornar apenas o atributo id do response body
1 // codigo com CPF e objeto simulacao omitido2 intid=3 given().4 pathParam("cpf",cpf).5 when().6 get("/simulacoes/{cpf}").7 then().8 extract().9 path("id");
Duas coisas muito importantes:
No final do código utilizamos o método path() passando como parâmetro o nome do atributo id
Sempre que extraímos uma informação, devemos retorná-la no tipo de dados correto, o retorno é do código inteiro da requisição, ou seja, desde o given()
Para o retorno foi criado um atributo do tipo int (inteiro) chamado id. O retorno inicia desde o given().
Extraindo o response body e associando-o a um objeto
Você notou que o response body tem exatamente os mesmos atributos que o nosso objeto Simulacao. Não criamos o objeto Simulacao de propósito: é sempre uma boa prática criar os objetos com os mesmos atributos de retorno.
Quando os atributos são os mesmos podemos extrair o response body direto para o objeto sem a necessidade de código. O Rest-Assured identifica isso e faz essa ligação.
Object Mapping
Há duas formas de mapeamento de objetos que o Rest-Assured nos ajuda (na verdade ele faz este trabalho por nós), para facilitar a escrita do teste. As formas são:
serialização (serialisation): transforma uma objeto em um JSON
deserialização (deserialisation): transforma um JSON em um objeto
A serialização já foi feita no exercício anterior, onde nós colocamos o objeto simulacao como parâmetro do body. A deserialização é feita extraindo o response body, que veremos agora
Continuação sobre “extraindo o response body…”
Como os dados são retornados no response body utilizaremos o método extract() e depois o métod body(), indicando que queremos o conteúdo da body. Para fazer a associação com o objeto é necessário inserir, depois do método body() o método as() e passar como parâmetro a classe que será deserializada. A composição da classe é sempre o nome da classe, um ponto . e a palavra class.
No exemplo acima um get() está sendo enviado com um CPF, no then() estamos extraindo o retorno como um body como um objeto Simulacao (última linha). É necessário, no início do given() criar o objeto Simulacao e associá-lo ao retorno (primeira linha).
Agora temos o objeto Simulacao com todos os dados do response body. Basta agora alterarmos os dados necessários utilizando os métodos setters.
1 // alteracao dos dados necessarios no objeto Simulacao2 simulacao.setNome("Nome alterado");3 simulacao.setEmail("email.alterado@gmail.com");
No exemplo de código acima alteramos o nome e email antes de usar o objeto como parâmetro do body.
Efetuando uma requisição DELETE
Sabemos que o método DELETE remove endpoint.
Normalmente um parâmetro, que é um identificador único do registro, é informado como path parameter.
Exemplo: DELETE /v1/api/simulacaoes/{cpf}
É necessário verificar na documentação da API qual parâmetro é necessário informar para que a remoção do endpoint seja executada. O Rest-Assured utiliza o método delete() que deve ser utilizado depois do método then().
Logo necessitamos apenas validar o statusCode() como 204 após o método then() como forma de validação da remoção com sucesso.
Criando um DELETE sem dependência de dados
Um problema que pode acontecer na execução de um script de remoção é o registro não existir mais. Podemos resolver de diversas formas… uma utilizando o próprio Rest-Assured, dentro deste contexto de aplicação, seria:
criar um objeto Simulacao
efetuar um método POST para cadastrar a simulação
extrair o CPF da simulação cadastrada
efetuar uma requisição DELETE utilizando o CPF extraido
Essa forma primeiro cria um endpoint (objeto simulação) e depois o remove.
Um exemplo, genérico (para não dar a resposta do próximo exercício para você) seria:
1 @Test 2 publicvoidremoverSimulacaoSemDependenciaDeDados(){ 3 4 // criacao do objeto que sera cadastrado com o builder 5 Objetoobjeto=Objeto.builder()... 6 7 // criacao de um endpoint e extracao de um atributo 8 StringatributoExtraido= 9 given().10 contentType(ContentType.JSON).11 body(objeto).12 when().13 post("/controller").14 then().15 statusCode(201).16 extract().17 body().18 path("atributo");19 20 // execucao da remocao, utilizando como chave o atributo extraido da requisicao \21 decriacao22 given().23 pathParam("atributo",atributoExtraido).24 when().25 delete("/controller/{atributo}").26 then().27 statusCode(204);28 }