Testes Automatizados para uma API REST

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

Você pode acessar a página principal do Rest-Assured em: http://rest-assured.io

Dependências necessárias

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:

git clone https://github.com/eliasnogueira/projeto-teste-api-livro-v1.git

Exemplo git clone

Se você deseja efetuar o download do projeto:

  1. Acesse https://github.com/eliasnogueira/projeto-teste-api-livro-v1
  2. Clique no botão Code
  3. Clique no item Download zip
Download do projeto

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.

 1 import org.junit.jupiter.api.Test;
 2 
 3 import static io.restassured.RestAssured.*;
 4 import static org.hamcrest.CoreMatchers.*;
 5 	
 6 class MeuTeste {
 7 	
 8     // teste
 9     @Test
10     void exemploDeTeste() {
11     }
12 	
13 }

Imports

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-Assured
2 import static io.restassured.RestAssured.*;
3 
4 // import dos possíveis métodos de validação dos resultados esperados
5 import static org.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 5
2 @Test
3 void exemploDeTeste() {
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 class UmaClasseDeTestePorTeste {
2 	
3     // você terá apenas um único teste na classe
4     @Test
5     void unicoTesteNaClasse() {
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.

 1 class DiversosTestesEmUmaClasse {
 2 	
 3     // você pode ter
 4     @Test
 5     void verificaSubmissaoComSucesso() {
 6     }
 7     
 8     // quantos testes
 9     @Test
10     void verificaUmaRegraDeNegocio() {
11     }
12     
13     // forem necessários
14     @Test
15     void verificaUmCenarioNegativo() {
16     }
17 	
18 }

Benefícios

  • Manutenção centralizada
  • Menor gestão de pré e pós condições
  • Correlação entre testes com o mesmo propósito
  • 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:

1 when().
2    get("http://localhost:8088/api/v1/simulacoes")

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.

 1 import org.junit.jupiter.api.Test;
 2 
 3 import static io.restassured.RestAssured.*;
 4 import static org.hamcrest.CoreMatchers.*;
 5 
 6 class ExemploGetTest {
 7 
 8     @Test
 9     void exemploDeTeste() {
10         when().
11             get("http://localhost:8088/api/v1/simulacoes").
12         then().
13             statusCode(200).
14             body("cpf", equalTo("66414919004"));
15     }
16 }

Efetuando uma requisição GET sem parâmetros

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
  • body: dados retornados pela requisição

Um exemplo de código com validações seria:

1 when().
2     get("http://localhost:8088/api/v1/simulacoes").
3 then().
4     statusCode(200).
5     body(algumaCoisaAqui);

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": 27 
4 }

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.

1 @Test
2 void exemploValidacaoDados() {
3     when().
4         get("http://localhost:8088/api/v1/simulacoes").
5     then().
6         statusCode(200).
7         body("nome", equalTo("João")).
8         body("idade", equalTo(27));
9 }

Note que, para cada atributo, teremos uma linha com o método body.

Body como um array de objetos

Teremos o caso de um body como um array de objetos quando:

  • a body inicia e termina com colchetes
  • a body possui objetos separados por vírgula (ou não no caso de apenas um registro)

O exemplo abaixo apresenta um retorno de body como um array contendo dois objetos:

 1 [
 2    {
 3       "nome": "João",
 4       "idade": 27 
 5    },
 6    {
 7       "nome": "Maria",
 8       "idade": 25 
 9    },   
10 ]

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.

 1 @Test
 2 void exemploValidacaoDados() {
 3     when().
 4         get("http://localhost:8088/api/v1/simulacoes").
 5     then().
 6         statusCode(200).
 7         body("[0].nome", equalTo("João")).
 8         body("[0].idade", equalTo(27)).
 9         body("[1].nome", equalTo("Maria")).
10         body("[1].idade", equalTo(25));        
11 }

Observações

Observação 1

Você precisa adicionar os seguintes imports

1 import static io.restassured.RestAssured.*;
2 import static org.hamcrest.CoreMatchers.*;

Observação 2

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.

Exemplo:

URL: GET /api/v1/restricoes/{cpf}

1 when().
2     get("http://localhost:8088/api/v1/restricoes/{cpf}", "66414919004")

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:

1 when().
2     get("http://localhost:8088/api/v1/restricoes/66414919004")

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.

Exemplo:

URL: GET /reservas/{codigoHotel}/{numeroQuarto}

1 when().
2     get("http://localhost:8088/reservas/{codigoHotel}/{numeroQuarto}", 10738, 215);

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

Exemplo:

URL: GET /reserva/{codigoHotel}/{numeroQuarto}

1 given().
2     pathParam("codigoHotel", 10738).
3     pathParam("numeroQuarto", 215).
4 when().
5     get("http://localhost:8088/reservas/{codigoHotel}/{numeroQuarto}");

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 endpoint GET /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:

  • o nome do atributo que será pesquisado
  • o valor do atributo para pesquisa

Exemplo:

URL: GET /api/v1/simulacoes?nome=Fulano

1 given().
2     queryParam("nome", "Fulano").
3 when().
4     get("http://localhost:8088/api/v1/simulacoes")

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.

1 when().
2     get("http://localhost:8088/api/v1/simulacoes?nome=Fulano")

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:

1 when().
2    get("http://localhost:8088/api/v1/simulacoes")

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:

1 RestAssured.baseURI = "http://localhost";
2 RestAssured.port = 8088;
3 RestAssured.basePath = "/api/v1";
4 
5 when().
6     get("/simulacoes")

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 public abstract class ApiBase {
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.

Este é o exemplo completo do que precisamos:

 1 import org.junit.jupiter.api.BeforeAll;
 2 
 3 import static io.restassured.RestAssured.*;
 4 
 5 public abstract class ApiBase {
 6 
 7     @BeforeAll
 8     static void preCondicao() {
 9         baseURI = "http://localhost";
10         port = 8080;
11         basePath = "/api/v1";
12     }
13 }

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.

Exemplo:

 1 class ResticoesGetTest extends ApiBase {
 2 
 3     @Test
 4     void teste1() {
 5         when().
 6            get("/simulacoes")
 7     }
 8     
 9     // teste
10     @Test
11     void teste2() {
12         when().
13            get("/simulacoes")
14     }
15 }

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 @Test
2 public void exemploPOST() {
3     given().
4         param("nomeDoParametro", "valorDoParametro"). // form parameter
5         body(objeto).                                 // body parameter
6     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
  1. o atributo/objeto simulacao é obrigatório, e o tipo de parâmetro é um body (request body)
  2. será necessário enviar os atributos descritos no exemplo
  3. 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.

1 JSONObject dados = new JSONObject();
2 dados.put("atributo", "valor");

O exemplo completo do JSONObject para os dados que precisamos enviar seria:

1 JSONObject dados = new JSONObject();
2 dados.put("nome", "Deltrano");
3 dados.put("cpf", "12345678930");
4 dados.put("email", "teste@gmail.com");
5 dados.put("valor", 22000);
6 dados.put("parcelas", 2);
7 dados.put("seguro", true);

Exemplo completo

Sempre que houver a necessidade de efetuar um POST com um body devemos seguir estes passos:

  1. Criar o objeto de dados para submissão (neste momento o JSONObject)
  2. 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 JSONObject dados = new JSONObject();
 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 objeto
12     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 Simulacao simulacao = new Simulacao("Deltrano", "12345678931", "teste@gmail.com", 22\
 3 000, 2, true);
 4 
 5 // instância do objeto inicializando os dados via setter
 6 Simulacao simulacao = new Simulacao();
 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.

Para ver o exemplo completo da classe, acesse este arquivo

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

Veja o exemplo de uma classe builder para a classe Simulacoes.

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:

 1 @Data
 2 @Builder
 3 @AllArgsConstructor
 4 @RequiredArgsConstructor
 5 public class Cadastro {
 6     
 7     private String cpf;
 8     private String nome;
 9     private String endereco;
10     private int idade;
11     private boolean restricao;
12     
13 }

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.

1 Cadastro cadastro = Cadastro.builder().
2         cpf("12345679013").
3         endereco("Av Assis Brasil").
4         idade(25).
5         restricao(false).
6         build();

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

Exemplo de código

1 @Test
2 public void exemploPUT() {
3     given().
4         pathParam("nomeDoParametro", "valorDoParametro").
5     when().
6         put("/simulacoes/{nomeDoParametro}");
7 }

Aspectos importantes

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.

Exemplo:

 1 String cpf = "66414919004";
 2 
 3 Simulacao simulacao = Simulacao.builder().
 4         nome("Nova pessoa").
 5         email("novo.email@gmail.com").
 6         cpf("66414919004").
 7         valor(11000).
 8         parcelas(3).
 9         seguro(true).
10         build();
11 
12 given().
13     pathParam("cpf", cpf).
14     contentType(ContentType.JSON).
15     body(simulacao).
16 when().
17     put("/simulacoes/{cpf}").

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.

Como base usaremos o response body abaixo:

1 {
2     "id": 11,
3     "nome": "Fulano",
4     "cpf": "66414919004",
5     "email": "fulano@gmail.com",
6     "valor": 11000,
7     "parcelas": 3,
8     "seguro": true
9 }

Extraindo parte do response body

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 omitido
2 int id = 
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.

1 Simulacao simulacao =
2     given().
3         pathParam("cpf", cpf).
4     when().
5         get("/simulacoes/{cpf}").
6     then().
7         extract().
8             body().
9                 as(Simulacao.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 Simulacao
2 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().

1 @Test
2 public void exemploDELETE() {
3     given().
4         pathParam("nomeDoParametro", "valorDoParametro").
5     when().
6         delete("/simulacoes/{nomeDoParametro}");
7 }

Por padrão um DELETE sempre deve retornar:

  • statusCode 204 (no content)
  • body vazia

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 public void removerSimulacaoSemDependenciaDeDados() {
 3 
 4     // criacao do objeto que sera cadastrado com o builder
 5     Objeto objeto = Objeto.builder()...
 6 
 7     // criacao de um endpoint e extracao de um atributo
 8     String atributoExtraido =
 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 de criacao
22     given().
23         pathParam("atributo", atributoExtraido).
24     when().
25         delete("/controller/{atributo}").
26     then().
27         statusCode(204);
28 }

Testes de Health Check para uma API REST

Exercício: criar um teste de Health Check

Testes de Contrato para uma API REST

Gerando um JSON Schema

Como aplicar um teste de contrato

Diferentes versões de API - parte 1

Diferentes versões de API - parte 2

Exercício: criar um teste de contrato