Testando o NGINX com SuperTest e Mocha

João Maia
pagarme
Published in
5 min readMar 19, 2020

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!

--

--