Motivação
Testar a infraestrutura não é um sonho mas sim uma realidade, se você ainda não faz nada a respeito saiba que já está atrasado. Com a transformação da infraestrutura em código acabando com os processos manuais e nem sempre documentados é preciso também trazer as boas práticas do mundo de desenvolvimento de software para a infraestrutura. Graças ao Docker subir uma representação da sua infraestrutura ficou mais fácil apesar de ainda não ser 100% fiel ao ambiente de produção.
Aqui na Pagar.me em parte de nossa arquitetura temos o NGINX controlando o fluxo de requisições para diversas aplicações e seguindo as boas práticas de testar, tinhamos uma base de teste usando Bash e httpie para testar essa infraestrutura. O problema com essa base de testes era a sua manutenção e confiança de novos desenvolvedores contribuirem, com isso tomamos a decisão de mudar para algo mais próximo ao nosso ambiente que é Node.js.
Ferramentas
Muitos já devem ter usado ou usam o NGINX como balanceador de carga da sua infraestrutura ou até para outros recursos mais avançados que ele permite com suas integrações de plugins, por exemplo, o projeto OpenResty. Se o seu NGINX tem diversos recursos e configurações particulares é preciso que você tenha controle disso e apenas comentar na configuração do arquivo talvez não seja o suficiente, caso se lembre de fazer isso. Com testes conseguimos garantir melhor o entendimento e propósito dessas configurações. Então, como escrever testes para isso?
Procurando alguma solução para execução de testes de chamadas HTTP acabamos encontrando como opção o SuperTest que é feito em JavaScript e podemos facilmente executar no nosso ambiente de integração contínua, usamos o Circle CI hoje. Apesar de, à primeira vista, ele parecer ser um framework voltado ao Express.js, ele pode ser executado para testar qualquer endpoint, sem se importar com a implementação por trás dele, exatamente o que precisamos. No próprio README do projeto tem um exemplo indicando essa forma de utilização.
Além disso, precisamos de uma ferramenta para gerenciar as execuções dos testes e fazer as asserções e gerar o relatório final. Dado que temos preferência por Node.js, escolhemos o Mocha que é um framework bem conhecido da comunidade JS. Quem já programou em Ruby e escreveu testes com RSpec vai achar bem parecido a estrutura de como organizar seus testes.
Como forma de validar e expor essa solução fiz um pequeno projeto para validação das ferramentas e conceito do que estavamos buscando. No desenho abaixo mostra como vai funcionar a estrutura do projeto. Explicando os componentes:
- Test runner: é o Docker que vai rodar a bateria de testes com o SuperTest;
- Gateway: é o NGINX (Docker) que queremos testar que está fazendo um papel de proxy reverso; e
- Echo: uma aplicação que responde uma resposta padrão para simular um backend.
Infraestrutura dos testes
Agora que já sabemos a estutura da infraestrutura que vamos testar precisamos tornar ela real. A escolha foi usar docker-compose que é uma ferramenta muito usada em ambiente de desenvolvimento hoje para subir o ambiente completo e simples:
version: '3.2'
services:
gateway:
hostname: gateway
build:
context: ./gateway
dockerfile: Dockerfile
depends_on:
- echo
links:
- echo
ports:
- 8080:80
networks:
- fake-vpc
echo:
hostname: echo
image: hashicorp/http-echo
command: -listen=:3000 -text="hello world"
ports:
- 3000:3000
networks:
- fake-vpc
test-runner:
build:
context: ./tests
dockerfile: Dockerfile
environment:
- GATEWAY_HOST=gateway
depends_on:
- gateway
links:
- gateway
networks:
- fake-vpc
networks:
fake-vpc:
Não temos novidades aqui para quem já está acostumado com o uso, declaramos nossos 3 serviços e fizemos as configurações devidas de rede para resolverem por nome dentro da rede virtualizada do Docker e exportamos as portas necessárias para testar de forma local o SuperTest ou simplesmente um curl
.
Gateway
O nosso gateway não tem muitas customizações nesse exemplo, foram apenas adicionados o proxy reverso para o echo
e uma configuração para gzip
:
gzip on;
Adicionando o echo
:
server {
listen 80;
server_name localhost; #charset koi8-r;
#access_log /var/log/nginx/host.access.log main; location / {
root /usr/share/nginx/html;
index index.html index.htm;
} location /echo {
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_pass http://echo:3000;
} // omitido o restante
}
Os testes
Não tem muito mistério, primeiro você tem que inicializar o executor do SuperTest para fazer as chamadas HTTP no Gateway e por fim descrever os testes usando o Mocha:
'use strict';
const request = require('supertest');
const assert = require('assert');
const GATEWAY_HOST = process.env.GATEWAY_HOST || 'http://localhost:8080'
const runner = request(GATEWAY_HOST);
describe('Gateway tests', () => {
it('Root path must return HTTP/200', (done) => {
runner
.get('/')
.expect(200)
.end(done);
});
it('Non existis path must return HTTP/404', (done) => {
runner
.get('/404')
.expect(404)
.end(done);
});
describe('Headers configurations', (done) => {
it('Server must be NGINX', (done) => {
runner
.get('/')
.expect((res) => {
assert.equal(res.header.server, 'nginx/1.17.9');
})
.end(done);
});
it('Content-Type must be HTML', (done) => {
runner
.get('/')
.expect((res) => {
assert.equal(res.header['content-type'], 'text/html');
})
.end(done);
});
it('Connection must be closed', (done) => {
runner
.get('/')
.expect((res) => {
assert.equal(res.header.connection, 'close');
})
.end(done);
});
it('Content encoding must be gzip', (done) => {
runner
.get('/')
.expect((res) => {
assert.equal(res.header['content-encoding'], 'gzip');
})
.end(done);
});
});
describe('Application path', (done) => {
it('/echo must return to application', (done) => {
runner
.get('/echo')
.expect((res) => {
assert.equal(res.text, 'hello world\n');
})
.end(done);
});
})
});
Olhando em detalhes para entender melhor:
it('Content encoding must be gzip', (done) => {
runner
.get('/')
.expect((res) => {
assert.equal(res.header['content-encoding'], 'gzip');
})
.end(done);
});
O nosso executor, no caso a variável runner
, executa uma chamada HTTP/GET na raíz do nosso Gateway e de resultado esperamos que o Content-Encoding
seja gzip
como bem definimos no arquivo de configuração e por fim fechamos as validações do teste com o .end(done)
. Detalhe importante, lembre-se que as chaves dos valores do Header
não são case-sensitive, o que isso significa, você pode enviar sua requisição escrito Content-Encoding
ou content-encoding
que não faz diferença, são tratados iguais. Afim de evitar confusão, o SuperTest padroniza tudo no minúsculo.
Colocando no CI
Como foi informado no começo, vamos usar o Circle CI para isso e a configuração dele é bem simples. Aproveitando a criação de testes foi adicionado também um teste no CI para ver ser o Dockerfile está seguindo as boas práticas com o Hadolint que não vamos entrar em detalhes nesse artigo, veja como ficou:
version: 2.1
jobs:
build:
machine: true
steps:
- checkout
- run:
name: Dockerfile lint 'gateway'
command:
docker run --rm -i hadolint/hadolint < ./gateway/Dockerfile
- run:
name: Test 'gateway' image
command: |
docker-compose up -d echo gateway
docker-compose up test-runner
Será que funcionou? Veja a imagem do CI:
Teste verde é só fazer deploy em produção agora! Espero que esse artigo tenha ajudado quem busca solução parecida e se tiver sugestões de melhorias é só comentar no artigo.
Se ficou interessado em trabalhar com essas tecnologias temos vagas abertas na Pagar.me para diversas áreas, só entrar aqui e mandar seu pedido de participação no processo seletivo.
Até o próximo artigo!