Testes automatizados de microsserviços no Docker para integração contínua

Em projetos relacionados ao desenvolvimento de arquitetura de microsserviços, CI/CD passa de uma possibilidade desejável para uma necessidade urgente. O teste automatizado é uma parte integrante da integração contínua, e uma abordagem adequada pode proporcionar à equipe muitas noites agradáveis com a família e amigos. Caso contrário, o projeto corre o risco de nunca ser concluído.

É possível cobrir todo o código do microsserviço com testes unitários usando objetos simulados, mas isso resolve apenas parcialmente o problema e deixa várias questões e complexidades em aberto, especialmente ao testar a manipulação de dados. Como sempre, os desafios mais críticos são: teste de consistência de dados em bancos de dados relacionais, teste de integração com serviços de nuvem e suposições incorretas ao escrever objetos simulados.

Tudo isso e um pouco mais pode ser resolvido testando o microsserviço completo em um contêiner Docker. Uma vantagem inegável para garantir a validade dos testes é que as mesmas imagens Docker que vão para a produção são submetidas aos testes.

A automação desse tipo de abordagem apresenta uma série de problemas, cuja resolução será descrita a seguir:

  • conflitos de tarefas paralelas em um único host Docker;
  • conflitos de identificadores no banco de dados durante as iterações do teste;
  • aguardando a prontidão dos microsserviços;
  • agregação e saída de logs para sistemas externos;
  • teste de requisições HTTP de saída;
  • teste de WebSockets (usando SignalR);
  • teste de autenticação e autorização OAuth.

Este é um artigo inspirado na minha apresentação no SECR 2019. Portanto, para aqueles que estão com preguiça de ler, aqui está a gravação da apresentação..

Testes automatizados de microsserviços no Docker para integração contínua

No artigo, explicarei como usar um script para iniciar em Docker o serviço testado, o banco de dados e os serviços da Amazon AWS, em seguida, executar os testes no Postman e, após a conclusão, parar e remover os contêineres criados. Os testes são realizados a cada mudança no código. Assim, garantimos que cada versão funcione corretamente com o banco de dados e os serviços da AWS.

O mesmo script é executado tanto pelos desenvolvedores em suas máquinas Windows quanto pelo servidor Gitlab CI em Linux.

Para que a implementação de novos testes seja justificável, não deve exigir a instalação de ferramentas adicionais, nem no computador do desenvolvedor, nem no servidor onde os testes são executados no commit. O Docker resolve essa questão.

O teste deve funcionar em um servidor local por várias razões:

  • Uma rede nunca é absolutamente confiável. De mil solicitações, uma pode não passar;
    Um teste automático, nesse caso, não passará, a execução será interrompida e será necessário buscar a causa nos logs;
  • Solicitações muito frequentes não são permitidas por alguns serviços de terceiros.

Além disso, é indesejável utilizar um ambiente de teste porque:

  • Tanto um código defeituoso executado nele quanto dados que o código correto não consegue processar podem danificar o ambiente;
  • Por mais que tentemos reverter todas as alterações feitas pelo teste, algo pode dar errado durante o teste em si (caso contrário, por que testar?).

Sobre o projeto e a organização do processo

Nossa empresa desenvolveu um aplicativo web baseado em microsserviços, operando no Docker na nuvem Amazon AWS. O projeto já utilizava testes unitários, no entanto, frequentemente surgiam erros que os testes unitários não detectavam. Era necessário testar um microsserviço completo junto com a base de dados e os serviços da Amazon.

O projeto adota um processo padrão de integração contínua, que inclui a testagem do microsserviço a cada commit. Após a designação da tarefa, o desenvolvedor faz alterações no microsserviço, testa manualmente e executa todos os testes automáticos disponíveis. Se necessário, o desenvolvedor modifica os testes. Se nenhum problema é encontrado, um commit é realizado na branch da tarefa. Após cada commit, os testes são executados automaticamente no servidor. A mesclagem na branch principal e a execução dos testes automáticos nela ocorrem após uma revisão bem-sucedida. Se os testes na branch principal passaram, o serviço é automaticamente atualizado no ambiente de teste no Amazon Elastic Container Service (ambiente de teste). O ambiente é necessário para todos os desenvolvedores e testadores, e danificá-lo é indesejável. Os testadores verificam correções ou novas funcionalidades neste ambiente, realizando testes manuais.

Arquitetura do projeto

Testes automatizados de microsserviços no Docker para integração contínua

O aplicativo consiste em mais de dez serviços. Alguns deles estão escritos em .NET Core e outros em NodeJs. Cada serviço funciona em um contêiner Docker no Amazon Elastic Container Service. Cada um possui seu próprio banco de dados Postgres, e alguns têm também Redis. Não há bancos de dados compartilhados. Se vários serviços precisam dos mesmos dados, esses dados, no momento de sua alteração, são enviados para cada um desses serviços através do SNS (Simple Notification Service) e SQS (Amazon Simple Queue Service), e os serviços os salvam em seus bancos de dados isolados.

SQS e SNS

O SQS permite colocar mensagens na fila e ler mensagens da fila via protocolo HTTPS.

Se vários serviços leem uma mesma fila, cada mensagem chega apenas a um deles. Isso é útil ao executar várias instâncias de um mesmo serviço para distribuir a carga entre elas.

Se for necessário que cada mensagem seja entregue a vários serviços, cada destinatário deve ter sua própria fila, e para a duplicação de mensagens em várias filas, é necessário o SNS.

No SNS, você cria um tópico e se inscreve nele, por exemplo, uma fila SQS. Mensagens podem ser enviadas para o tópico. Assim, a mensagem é enviada para cada fila que está inscrita nesse tópico. No SNS, não existe um método para ler mensagens. Se, durante a depuração ou testes, for preciso saber o que está sendo enviado ao SNS, é possível criar uma fila SQS, inscrevê-la no tópico desejado e ler a fila.

Testes automatizados de microsserviços no Docker para integração contínua

API Gateway

A maioria dos serviços não está acessível diretamente pela internet. O acesso é feito através do API Gateway, que verifica os direitos de acesso. Este também é nosso serviço e ele também possui testes.

Notificações em tempo real

O aplicativo usa SignalR, para mostrar ao usuário notificações em tempo real. Isso é implementado no serviço de notificações. Ele está disponível diretamente pela internet e funciona com OAuth, pois integrar o suporte a Web Sockets no Gateway revelou-se pouco prático em comparação com a integração do OAuth e do serviço de notificações.

Uma abordagem conhecida para testes

Os testes unitários substituem objetos reais por objetos simulados, como bancos de dados. Se um microserviço, por exemplo, tentar criar um registro em uma tabela com uma chave estrangeira, e o registro ao qual essa chave se refere não existir, então a solicitação não pode ser executada. Os testes unitários não conseguem detectar isso.

Em artigo da Microsoft sugere usar um banco de dados em memória e injetar objetos simulados.

O banco de dados em memória é um dos SGBDs suportados pelo Entity Framework. Ele foi criado especificamente para testes. Os dados em um banco desse tipo são mantidos apenas até a conclusão do processo que o utiliza. Não é necessário criar tabelas, e a integridade dos dados não é verificada.

Objetos mock simulam uma classe substituível na medida em que o desenvolvedor do teste entende seu funcionamento.

O artigo da Microsoft não menciona como configurar o PostgreSQL para iniciar automaticamente e realizar migrações ao iniciar o teste. Minha solução faz isso e, além disso, não adiciona nenhum código específico para testes ao microsserviço.

Vamos à solução

Durante o desenvolvimento, ficou claro que testes unitários não eram suficientes para identificar todos os problemas a tempo, então foi decidido abordar a questão de uma maneira diferente.

Configuração do ambiente de teste

A primeira tarefa é configurar o ambiente de teste. As etapas necessárias para iniciar o microsserviço são:

  • Configurar o serviço a ser testado para o ambiente local, especificando as credenciais de conexão ao banco de dados e AWS nas variáveis de ambiente;
  • Iniciar o PostgreSQL e realizar a migração, rodando o Liquibase.
    Nos SGBDs relacionais, antes de gravar dados no banco, é necessário criar um esquema de dados, ou seja, tabelas. Quando uma aplicação é atualizada, as tabelas devem ser adaptadas ao formato usado pela nova versão, preferencialmente sem perda de dados. Isso é chamado de migração. A criação de tabelas em um banco inicialmente vazio é um caso específico de migração. A migração pode ser integrada na própria aplicação. Tanto no .NET quanto no NodeJS, existem frameworks para migração. No nosso caso, por questões de segurança, os microsserviços não têm permissão para modificar o esquema de dados, e a migração é realizada com o Liquibase.
  • Iniciar o Amazon LocalStack. Esta é uma implementação dos serviços AWS para execução local. Existe uma imagem pronta do LocalStack no Docker Hub.
  • Executar um script para criar as entidades necessárias no LocalStack. Scripts Shell utilizam AWS CLI.

Para os testes do projeto, está sendo utilizado Postman. Ele já existia antes, mas era executado manualmente e testava a aplicação que já estava implantada no ambiente. Esta ferramenta permite fazer solicitações HTTP(S) arbitrárias e verificar se as respostas estão de acordo com as expectativas. As solicitações são agrupadas em uma coleção, e é possível executar toda a coleção de uma vez.

Testes automatizados de microsserviços no Docker para integração contínua

Como funciona o teste automático

Durante o teste no Docker, tudo funciona: tanto o serviço testado, quanto o Postgres, o ferramenta de migração e o Postman, ou melhor, sua versão de console – Newman.

O Docker resolve uma série de problemas:

  • Independência da configuração do host;
  • Instalação de dependências: o docker baixa imagens do Docker Hub;
  • Retorno do sistema ao estado original: basta remover os contêineres.

Docker-compose une contêineres em uma rede virtual, isolada da internet, onde os contêineres se encontram pelo nome de domínio.

O teste é gerenciado por um script shell. Para executar o teste no Windows, usamos o git-bash. Assim, basta um único script para Windows e Linux. Git e Docker estão instalados em todos os desenvolvedores do projeto. Ao instalar o Git no Windows, o git-bash é instalado, então todos também o têm.

O script executa os seguintes passos:

  • Construção das imagens do Docker
    docker-compose build
  • Inicialização do banco de dados e LocalStack
    docker-compose up -d
  • Migração do banco de dados e preparação do LocalStack
    docker-compose run
  • Inicialização do serviço testado
    docker-compose up -d
  • Execução do teste (Newman)
  • Parar todos os contêineres
    docker-compose down
  • Publicação dos resultados no Slack
    Temos um chat onde as mensagens aparecem com um check verde ou um x vermelho e um link para o log.

Nestes passos, estão envolvidos os seguintes imagens Docker:

  • O serviço testado é a mesma imagem que a de produção. A configuração para o teste é feita através de variáveis de ambiente.
  • Para Postgres, Redis e LocalStack, usamos imagens prontas do Docker Hub. Para Liquibase e Newman, também existem imagens prontas. Nós construímos as nossas com base nas delas, adicionando nossos arquivos.
  • Para a preparação do LocalStack, usamos uma imagem pronta do AWS CLI, e com base nela, é criada uma imagem contendo o script.

Usando volumes, não é necessário construir uma imagem Docker apenas para adicionar arquivos ao contêiner. No entanto, volumes não são adequados para nosso ambiente, porque as tarefas do Gitlab CI operam em contêineres. A partir de tal contêiner, é possível gerenciar o Docker, mas volumes montam pastas apenas do sistema host, e não de outro contêiner.

Problemas que podem surgir

Aguardando prontidão

Quando o contêiner com o serviço é iniciado, isso não significa que ele está pronto para aceitar conexões. É preciso aguardar a conexão para continuar.

Às vezes, essa tarefa é resolvida com um script wait-for-it.sh, que aguarda a oportunidade de estabelecer uma conexão TCP. No entanto, o LocalStack pode retornar um erro 502 Bad Gateway. Além disso, ele consiste em vários serviços, e, se um deles estiver pronto, isso não diz nada sobre os outros.

Solução: scripts de preparação do LocalStack, que aguardam uma resposta 200 tanto do SQS quanto do SNS.

Conflitos de tarefas paralelas

Vários testes podem ser executados simultaneamente em um único host Docker, portanto, os nomes dos contêineres e redes devem ser únicos. Além disso, testes de diferentes branches de um mesmo serviço também podem ser executados ao mesmo tempo, por isso não é suficiente apenas especificar nomes exclusivos em cada arquivo compose.

Solução: o script define um valor único para a variável COMPOSE_PROJECT_NAME.

Particularidades do Windows

Ao usar o Docker no Windows, há várias coisas que gostaria de chamar sua atenção, uma vez que essa experiência é importante para entender as causas dos erros.

  1. Os scripts de shell dentro do contêiner devem ter finais de linha no formato Linux.
    O símbolo CR para o shell é um erro de sintaxe. A partir da mensagem de erro, é difícil entender que esse é o problema. Ao editar esses scripts no Windows, é necessário usar um editor de texto adequado. Além disso, o sistema de controle de versões deve estar configurado corretamente.

Aqui está como configurar o git:

git config core.autocrlf input

  1. O Git-bash emula as pastas padrão do Linux e, ao chamar arquivos exe (incluindo docker.exe), substitui os caminhos absolutos do Linux por caminhos do Windows. No entanto, isso não faz sentido para caminhos que não estão na máquina local (ou caminhos dentro do contêiner). Esse comportamento não pode ser desativado.

Solução: adicionar uma barra adicional no início do caminho: \/\/bin em vez de \/bin. O Linux entende tais caminhos; para ele, várias barras são consideradas como uma. Mas o git-bash não reconhece esses caminhos e não tenta transformá-los.

Saída de logs

Ao executar os testes, gostaria de ver os logs tanto do Newman quanto do serviço sendo testado. Como os eventos desses logs estão interligados, combiná-los em um único console é muito mais conveniente do que ter dois arquivos separados. O Newman é iniciado através de docker-compose run, e, portanto, sua saída vai para o console. Resta fazer com que a saída do serviço também vá para lá.

A solução inicial consistia em fazer docker-compose up sem a flag -d, mas, usando as funcionalidades do shell, enviar esse processo para o fundo:

docker-compose up <service> &

Isso funcionou até que foi necessário enviar os logs do Docker para um serviço externo. docker-compose up parou de gerar logs no console. No entanto, o comando funcionava docker attach.

Solução:

docker attach --no-stdin ${COMPOSE_PROJECT_NAME}__1 &

Conflito de identificadores durante as iterações do teste

Os testes são executados em várias iterações. O banco de dados, por sua vez, não é limpo. As entradas no banco possuem IDs únicos. Se IDs específicos forem escritos nas solicitações, na segunda iteração obteremos um conflito.

Para evitá-lo, ou os IDs devem ser únicos, ou todos os objetos criados pelo teste devem ser removidos. Não é permitido remover alguns objetos, de acordo com os requisitos.

Solução: gerar GUIDs com scripts no Postman.

var uuid = require('uuid');
var myid = uuid.v4();
pm.environment.set('myUUID', myid);

Depois, na solicitação, use o símbolo {{myUUID}}, que será substituído pelo valor da variável.

Interação através do LocalStack

Se o serviço testado lê de uma fila SQS ou escreve nela, então para verificar isso, o teste também deve interagir com essa fila.

Solução: solicitações do Postman para o LocalStack.

A API dos serviços da AWS é documentada, o que permite fazer solicitações sem SDK.

Se o serviço escreve na fila, nós a lemos e verificamos o conteúdo da mensagem.

Se o serviço envia mensagens para o SNS, na fase de preparação o LocalStack cria também uma fila e se inscreve neste tópico SNS. Em seguida, tudo se resume ao que foi descrito acima.

Se o serviço precisa ler uma mensagem da fila, então no passo anterior do teste, escrevemos essa mensagem na fila.

Testando as solicitações HTTP que saem do microsserviço testado

Alguns serviços operam via HTTP com algo além da AWS, e algumas funcionalidades da AWS não são implementadas no LocalStack.

Solução: nesses casos, pode ajudar MockServer, que possui uma imagem pronta em Docker Hub. As solicitações e respostas esperadas são configuradas através de uma solicitação HTTP. A API é documentada, então fazemos solicitações do Postman.

Testando autenticação e autorização OAuth

Utilizamos OAuth e JSON Web Tokens (JWT). Para o teste, precisamos de um provedor OAuth que possamos executar localmente.

Toda a interação do serviço com o provedor OAuth se resume a duas solicitações: primeiro se solicita a configuração /.well-known/openid-configuration, e depois se solicita a chave pública (JWKS) no endereço da configuração. Tudo isso é conteúdo estático.

Solução: nosso provedor OAuth de teste é um servidor de conteúdo estático e dois arquivos nele. O token foi gerado uma vez e commitado no Git.

Particularidades do teste do SignalR

Os web sockets não funcionam com o Postman. Para testar o SignalR, uma ferramenta especial foi criada.

O cliente do SignalR pode ser mais do que apenas um navegador. Existe uma biblioteca cliente para .NET Core. O cliente, escrito em .NET Core, estabelece a conexão, passa pela autenticação e espera por uma sequência específica de mensagens. Se uma mensagem inesperada for recebida ou se a conexão for encerrada, o cliente termina com o código 1. Ao receber a última mensagem esperada, ele encerra com o código 0.

Simultaneamente ao cliente, o Newman está em execução. Vários clientes são iniciados para garantir que as mensagens sejam entregues a todos que precisam.

Testes automatizados de microsserviços no Docker para integração contínua

Para iniciar vários clientes, a opção —scale é usada na linha de comando do docker-compose.

Antes de iniciar o Postman, o script aguarda até que todos os clientes estabeleçam a conexão.
Já encontramos problemas de espera por conexão. Mas lá eram servidores, aqui é um cliente. É necessário um enfoque diferente.

Solução: o cliente no contêiner utiliza o mecanismo HealthCheck, para informar ao script no host seu status. O cliente cria um arquivo em um caminho determinado, por exemplo, /healthcheck, assim que a conexão é estabelecida. O script HealthCheck no Dockerfile se parece com isto:

HEALTHCHECK --interval=3s CMD if [ ! -e /healthcheck ]; then false; fi

Time docker inspect mostra para o contêiner o status normal, o status de saúde e o código de saída.

Após a conclusão do Newman, o script verifica se todos os contêineres com o cliente foram encerrados, e se foi com o código 0.

A felicidade existe

Depois de superarmos as dificuldades descritas acima, temos um conjunto de testes funcionando de forma estável. Nos testes, cada serviço opera como um todo, interage com o banco de dados e com o Amazon LocalStack.

Esses testes protegem a equipe de mais de 30 desenvolvedores de erros em aplicativos com interações complexas de mais de 10 microsserviços durante implantações frequentes.

Fonte: habr.com

Compre hospedagem confiável para sites com proteção contra DDoS, servidores VPS VDS 🔥 Compre hospedagem confiável para sites com proteção contra DDoS, servidores VPS VDS | ProHoster