Projeto para estudo e definição de uma arquitetura base para testes automatizados de API Rest com Cypress.
- Seja maneiro (a), faça referência ao utilizar esta arquitetura/repositório ✌️;
- Instale o Node.js;
- Baixe este repositório ou faça um git clone;
- Abra o diretório do projeto e execute o comando:
npm install
- Para abrir a interface de execução do Cypress, execute no diretório do projeto:
npx cypress open
- Próximo passo é configurar/instalar a API ServeRest na sua máquina;
- A nossa API alvo deste projeto é a ServeRest localmente, para utiliza-la execute a aplicação via npm ou via Docker.
- Para mais detalhes visite o repositório oficial do ServeRest.
cypress4testautomationapi/
├─ cypress/
│ │
│ ├── fixtures/
│ │ ├── *.json
│ │ ├── *.csv
│ │ └── *.png
│ │
│ ├── integration/
│ │ ├── <categoria>/
│ │ │ └── <categoria>Tests.spec.js
│ │ └── <categoria2>/
│ │ └── <categoria2>Tests.spec.js
│ │
│ ├── plugins/
│ │ └── index.js
│ │
│ ├── reports/
│ │ └── mocha/
│ │ └── mochafiles (*.json, *html)
│ │
│ ├── support/
│ │ ├── databaseCommands.js
│ │ ├── apiGeneralCommands.js
│ │ ├── api<Categoria>Commands.js
│ │ ├── api<Categoria2>Commands.js
│ │ └── index.js
│ │
│ └── videos/
│
├── environmentsConfig/
├── node_modules/
├── cypress.json
├── package-lock.json
├── package.json
└── README.md
- fixtures: arquivos para massa de dados estática para os testes (csv, png, xlsx, txt);
- integration: arquivos de testes separados em categorias/módulos da API para facilitar a organização. Extensão *.spec.js;
- plugins: plugins que são utilizados na solução ficam dentro do arquivo "plugins/index.js";
- reports: diretório com o relatório de execução dos testes usando Mocha Awesome;
- support: camada com comandos Cypress customizados e sobrescritas globais:
- Mapeamento das requisições (headers, requestservice, parametros [body, path, queryString]) para reuso em diferentes testes.
- Arquivo para comandos de select/insert em banco de dados.
- Arquivo index.js responsável de receber as importações dos comandos Cypress;
- videos: geração opcional de videos das execução dos testes;
- environmentsConfig: diretório com os arquivos de configuração por ambiente;
- node_modules: arquivos ou diretórios que podem ser carregados pelo Node.js;
- cypress.json: arquivo de configuração do Cypress;
- package-lock.json: gerado automaticamente com as instalações e atualizações de pacotes;
Requests como commands
Cada endpoint é mapeado com a sua estrutura (headers, parâmetros, método, endpoint, cookies) no Cypress commands para focarmos em reuso. Os arquivos de mapeamento de requisições podem ser feitos por módulo/categoria. Exemplo:No exemplo vemos o mapeamento do endpoint Produtos para ser usado por todos os testes de API que desejam utiliza-lo. Para criar um teste com esta requisição basta utilizar o command referente e passar o(s) parametro(s):
it('Produtos - Buscar Produto Inexistente', ()=>{
cy.getProdutos('nome=9dj9128dh12h89')
.then(response =>{
expect(response.status).to.equal(200)
expect(response.body.quantidade).to.equal(0)
})
})
Testes isolados e e2e
Testes de requisição de maneira isolada para validar parâmetros válidos, inválidos, status code estão presentes nesta arquitetura: it('Produtos - Excluir Produto Inexistente',()=>{
cy.deleteProdutos("xxx", true)
.then(response =>{
expect(response.status).to.equal(200)
expect(response.body.message).to.eq("Nenhum registro excluído")
})
})
Testes de múltiplas requisições (e2e) podem ser feitos com esta arquitetura, veja exemplo de um teste para Deletar um Produto (produto é criado durante o teste):
it('Produtos - Excluir Produto Existente',()=>{
const produto ={
nome: faker.random.uuid(),
preco: faker.random.number(),
descricao: "Mouse bom",
quantidade: "5"
}
cy.postProdutos(produto)
.then(response =>{
expect(response.status).to.equal(201)
expect(response.body.message).to.equal("Cadastro realizado com sucesso")
let _id = response.body._id
cy.deleteProdutos(_id, true)
.then(respDelete =>{
expect(respDelete.status).to.equal(200)
expect(respDelete.body.message).to.eq("Registro excluído com sucesso")
})
cy.getProdutos('_id='+_id)
.then(respGet =>{
expect(respGet.status).to.equal(200)
expect(respGet.body.quantidade).to.equal(0)
})
})
})
Testes de exceção de status code (4xx e 5xx)
Para testes de exceção de status code (client side [4xx] or server side [5xx]) precisamos incluir um parâmetro failOnStatusCode na requisição com valor false.
Vide exemplo de mapeamento de requisição:
Cypress.Commands.add('deleteProdutos', (productId, failStatusCode) =>{
cy.api({
method: 'DELETE',
url: '/produtos/'+productId,
headers: { Authorization : localStorage.getItem('token') },
failOnStatusCode: failStatusCode
})
})
Vide exemplo de teste "forçando" um erro para validar o statuscode e response body:
it('Produtos - Excluir Produto token expirado',()=>{
localStorage.setItem('token', "token erradinho")
cy.deleteProdutos("xxx", false)
.then(response =>{
expect(response.status).to.equal(401)
expect(response.body.message).to.eq("Token de acesso ausente, inválido, expirado ou usuário do token não existe mais")
})
})
Mock de dados
Biblioteca Faker para mock de dados. Vide exemplos de dados que podem ser mascarados.
Data Driven Testing
A arte de reaproveitar o mesmo teste com o mesmo fluxo e asserção variando somente a massa de teste proveniente de dados estáticos ou arquivos (*.csv, *.json, *.xlsx), chamamos de Data Driven Testing (leia mais sobre), na arquitetura temos o uso de um arquivo json (JArray) para a massa de testes:
[
{
"nome": "Mouse Gamer Adamantiun Shinigami Usb",
"preco": 98,
"descricao": "Mouses para Jogos",
"quantidade": 12
},
{
"nome": "Monitor Gamer AOC Agon 32'' Curvo 165Hz",
"preco": 269,
"descricao": "Monitores Gamer",
"quantidade": 45
},
{
"nome": "Kit 3 Roteadores Gigabit Wifi TP-Link Rede Mesh AC1200",
"preco": 189,
"descricao": "Dispositivos de Conexão em Rede",
"quantidade": 78
}
]
O mesmo teste é criado N vezes através do arquivo json:
const produtos = require('../../fixtures/Produtos/produtosList.json')
const faker = require('faker')
//JArray (produtoList.json) com cada objeto a ser cadastrado
produtos.forEach(produto => {
it('Produtos - Cadastrar Produto DDT',()=>{
let expectedStatusCode = 201;
let expectedSuccessMessage = "Cadastro realizado com sucesso";
const produtoTestData ={
"nome": produto.nome + "-" + faker.random.number(),
"preco": produto.preco,
"descricao": produto.descricao,
"quantidade": produto.quantidade
}
cy.postProdutos(produtoTestData)
.then(response =>{
expect(response.status).to.equal(expectedStatusCode)
expect(response.body.message).to.equal(expectedSuccessMessage)
})
})
})
Mocha report customizado
Em desenvolvimento
Chai: asserção status code e response body
Podemos validar de diversas formas os dados retornados no response (body, cookies, headers, status code), vide exemplos que podem ser aplicados: clique aqui.
Exemplo de assertiva de status code e parâmetro "message" do response body com o método "to.equal":
expect(response.status).to.equal(expectedStatusCode)
expect(response.body.message).to.equal(expectedSuccessMessage)
Orquestração de métodos
A organização dos métodos que devem ser executados antes ou depois dos testes ou bateria pode ser feito através de méetodos nativos do Cypress, clique aqui para detalhes.
Um exemplo comum para testes de API é a geração de token de acesso a cada teste, veja exemplo abaixo do método que é executado antes de cada teste para garantir o acesso dos recursos com o token correto:
beforeEach(() => {
cy.generateTokenAsAdmin()
})
Neste caso, o Token é gerado como admin e usamos a Request as Command (../support/apiGeneralCommands.js), veja a requisição mapeada e já enviando o token para o storage para ser usado por todos os testes no header:
Cypress.Commands.add('generateTokenAsAdmin', () =>{
cy.api({
method: 'POST',
url: '/login',
body: {
"email": "fulano@qa.com",
"password": "teste"
}
})
.then(response =>{
expect(response.status).to.eql(200)
localStorage.setItem('token', response.body.authorization)
expect(localStorage.getItem('token')).not.null
cy.log(localStorage.getItem('token'))
})
})
Arquivo de configuração
Recurso nativo do Cypress através do arquivo cypress.json. Vide documentação oficial.
Variáveis globais por ambiente
Para modificar suas variáveis globais por ambiente temos uma pasta criada "cypress/environmentsConfig" com dois possíveis ambientes "Prod" e "Qa" representados pelos arquivos exampleProd.json e exampleQa.json respectivamente.
Foi feita a inclusão de um plugin (/cypress/plugins/index.js) através do método "getConfigurationByFile()" onde podemos alterar o ambiente ao executar pela linha de comando incluindo qual ambiente se deseja:
npx cypress run --env configfile=exampleProd
Geração e uso de token
Vide feature "Orquestração de métodos" para entender como o Token é gerado/orquestrado. Para o uso basta incluir o header na Request as Command e incluir o item do localStorage "token":
Cypress.Commands.add('postProdutos', bodyJson =>{
cy.api({
method: 'POST',
url: '/produtos',
body: bodyJson,
headers: { Authorization : localStorage.getItem('token') }})
})
Parametros via Json, QueryString e Path
Exemplo de uso de parâmetro Path com a requisição Delete Produtos:
Ao mapear a requisição (Request as Command) incluímos o parâmetro junto ao request service (parâmetro url):
Cypress.Commands.add('deleteProdutos', (productId, failStatusCode) =>{
cy.api({
method: 'DELETE',
url: '/produtos/'+productId,
headers: { Authorization : localStorage.getItem('token') },
failOnStatusCode: failStatusCode
})
})
Exemplo de uso de parâmetro QueryString com a requisição Get Produtos:
Ao mapear a requisição (Request as Command) incluímos o parâmetro junto ao request service (parâmetro url) devendo ser informado quais parâmetros concatenados na camada de testes (integration):
Cypress.Commands.add('getProdutos', queryString =>{
cy.api({
method: 'GET',
url: '/produtos?'+ queryString})
})
Este recurso ainda está em pesquisa para ser otimizado.
Exemplo de uso de parâmetro Json com a requisição Post Produtos:
Neste caso temos um json de envio, com os seguintes parâmetros:
{
"nome": "nome",
"preco": "1",
"descricao": "descricao",
"quantidade": "1"
}
Ao mapear a requisição (Request as Command) incluímos o parâmetro "body" com nossa estrutura de json "jsonBody". Nossos dados virão da camada de testes (integration):
Cypress.Commands.add('postProdutos', jsonBody =>{
cy.api({
method: 'POST',
url: '/produtos',
body: jsonBody,
headers: { Authorization : localStorage.getItem('token') }}) // header de autenticação
})
Camada de teste com o envio dos dados no teste, video a constante "produto" com os dados mockados:
it('Produtos - Cadastrar Produto',()=>{
const produto ={
"nome": faker.random.uuid(),
"preco": faker.random.number(),
"descricao": "Mouse bom",
"quantidade": "5"
}
cy.postProdutos(produto)
.then(response =>{
expect(response.status).to.equal(201)
expect(response.body.message).to.equal("Cadastro realizado com sucesso")
})
})
})
Pipeline de teste via Github Actions
Pipeline feito com Github Actions executado em máquina Linux com os processos:
- Instancia da aplicação ServeRest local via Docker
- Execução de todos os testes - Task nativa Cypress
Flows executados disponível aqui;
Arquivo yml disponível aqui;
Para novas features crie uma issue ou verifique o board do projeto.
Para novas contribuições, faça um fork do projeto, realize as alterações e submeta um Pull Request ou crie uma issue para ser avaliada;