Testando o NGINX com SuperTest e Mocha

Olá,
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 realizar esse processo apesar de ainda não ser 100% fiel ao ambiente de produção.
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 acabei encontrando como opção o SuperTest que é feito em JavaScript e podemos facilmente executar no nosso ambiente de integração contínua com Node.js. Apesar de, à primeira, vista ele ser um framework voltado ao Express.js, ele pode ser executado para testar qualquer aplicação Web sem se importar com o que seja, examente o que precisamos.
Além disso, precisamos de alguma ferramenta que vai gerenciar as execuções dos testes e fazer as asserções e gerar o relatório final, nessa caso, escolhi o Mocha que já tinha familiaridade trabalhando em projetos de Node.js no passado. Quem já programou em Ruby e escreveu testes com RSpec vai achar bem parecido.
Como forma de validar essa solução fiz um pequeno projeto para validação das ferramentas e conceito do que buscava. No desenho abaixo mostro 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.
+-----------------+ +-----------------+ +------------------+
| | | | | |
| TEST | | | | |
| +--------------> GATEWAY (NGINX) +-------------------> ECHO (BACKEND) |
| RUNNER | | | | |
| | | | | |
+-----------------+ +-----------------+ +------------------+
Infraestrutura dos testes
Agora que já sabemos a estutura da infraestrutura que vamos testar precimos tornar ela real. A escolha foi usar docker-compose que é uma ferramenta muito usada em ambiente de desenvolvimento hoje para subir o ambiente completo:
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 pelo 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, basicamente adicionei o proxy reverso para o echo
e adicionei 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 chamad 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 a Promise
do teste com o .end(done)
. Detalhe importante, lembre-se que as chaves dos valores do Header
não são sensíveis ao caso, 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
Tenho usado o Circle CI para isso pois é fácil o uso e para projetos abertos não tem custo e usamos ele na empresa. Acho ele bem completo para ajudar nisso mas acho que falta melhorar na parte de granularidade de acessos e permissões da organização e projetos. Aproveitando a brincadeira coloquei de quebra um teste no CI para ver ser o Dockerfile tá seguindo as boas práticas também com o Hadolint:
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:

Acho que era isso que queria passar nesse artigo, até o próximo.
Comments ()