Teste automatizado de microsserviços no Docker para integração contínua

Em projetos relacionados ao desenvolvimento de arquitetura de microsserviços, o CI/CD passa da categoria de oportunidade agradável para a categoria de necessidade urgente. Os testes automatizados são parte integrante da integração contínua, uma abordagem competente que 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 com objetos simulados, mas isso resolve apenas parcialmente o problema e deixa muitas dúvidas e dificuldades, principalmente ao testar o trabalho com dados. Como sempre, os mais urgentes são testar a consistência dos dados em um banco de dados relacional, testar o trabalho com serviços em nuvem e fazer suposições incorretas ao escrever objetos simulados.

Tudo isso e um pouco mais podem ser resolvidos testando todo o microsserviço em um contêiner Docker. Uma vantagem indiscutível para garantir a validade dos testes é que são testadas as mesmas imagens Docker que entram em produção.

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

  • conflitos de tarefas paralelas no mesmo host docker;
  • conflitos de identificador no banco de dados durante iterações de teste;
  • aguardando que os microsserviços estejam prontos;
  • mesclar e enviar logs para sistemas externos;
  • testar solicitações HTTP de saída;
  • teste de soquete web (usando SignalR);
  • testando autenticação e autorização OAuth.

Este artigo é baseado em meu discurso na SECR 2019. Então, para quem tem preguiça de ler, aqui está uma gravação do discurso.

Teste automatizado de microsserviços no Docker para integração contínua

Neste artigo vou te contar como usar um script para executar o serviço em teste, um banco de dados e serviços Amazon AWS no Docker, depois testar no Postman e, após concluídos, parar e deletar os containers criados. Os testes são executados sempre que o código muda. Dessa forma, garantimos que cada versão funcione corretamente com o banco de dados e serviços AWS.

O mesmo script é executado pelos próprios desenvolvedores em seus desktops Windows e pelo servidor Gitlab CI no Linux.

Para ser justificado, a introdução de novos testes não deveria exigir a instalação de ferramentas adicionais nem no computador do desenvolvedor nem no servidor onde os testes são executados em um commit. O Docker resolve esse problema.

O teste deve ser executado em um servidor local pelos seguintes motivos:

  • A rede nunca é totalmente confiável. Em mil solicitações, uma pode falhar;
    Nesse caso, o teste automático não funcionará, o trabalho será interrompido e você terá que procurar o motivo nos logs;
  • Solicitações muito frequentes não são permitidas por alguns serviços de terceiros.

Além disso, é indesejável usar o suporte porque:

  • Uma posição pode ser quebrada não apenas por códigos incorretos executados nela, mas também por dados que o código correto não consegue processar;
  • Não importa o quanto tentemos reverter todas as alterações feitas pelo teste durante o teste em si, algo pode dar errado (caso contrário, por que testar?).

Sobre a organização do projeto e do processo

Nossa empresa desenvolveu uma aplicação web de microsserviço rodando em Docker na nuvem Amazon AWS. Os testes unitários já foram usados ​​no projeto, mas muitas vezes ocorriam erros que os testes unitários não detectavam. Foi necessário testar todo um microsserviço junto com o banco de dados e os serviços da Amazon.

O projeto usa um processo padrão de integração contínua, que inclui testar o microsserviço a cada commit. Após atribuir uma tarefa, o desenvolvedor faz alterações no microsserviço, testa-o manualmente e executa todos os testes automatizados disponíveis. Se necessário, o desenvolvedor altera os testes. Se nenhum problema for encontrado, um commit será feito no branch deste problema. Após cada commit, os testes são executados automaticamente no servidor. A fusão em um branch comum e o lançamento de testes automáticos nele ocorrem após uma revisão bem-sucedida. Se os testes na ramificação compartilhada forem aprovados, o serviço será atualizado automaticamente no ambiente de teste no Amazon Elastic Container Service (bench). O estande é necessário para todos os desenvolvedores e testadores e não é aconselhável quebrá-lo. Os testadores neste ambiente verificam uma correção ou um novo recurso realizando testes manuais.

Arquitetura do projeto

Teste automatizado de microsserviços no Docker para integração contínua

O aplicativo consiste em mais de dez serviços. Alguns deles são escritos em .NET Core e outros em NodeJs. Cada serviço é executado em um contêiner Docker no Amazon Elastic Container Service. Cada um tem seu próprio banco de dados Postgres e alguns também possuem Redis. Não existem bancos de dados comuns. Se vários serviços precisarem dos mesmos dados, então esses dados, quando alterados, são transmitidos para cada um desses serviços via SNS (Simple Notification Service) e SQS (Amazon Simple Queue Service), e os serviços os salvam em seus próprios bancos de dados separados.

SQS e SNS

SQS permite colocar mensagens em uma fila usando o protocolo HTTPS e ler mensagens da fila.

Se vários serviços lerem uma fila, cada mensagem chegará apenas a um deles. Isto é útil ao executar várias instâncias do mesmo serviço para distribuir a carga entre elas.

Se você deseja que cada mensagem seja entregue a vários serviços, cada destinatário deve ter sua própria fila e o SNS é necessário para duplicar as mensagens em várias filas.

No SNS você cria um tópico e se inscreve nele, por exemplo, uma fila SQS. Você pode enviar mensagens para o tópico. Neste caso, a mensagem é enviada para cada fila inscrita neste tópico. O SNS não possui um método para leitura de mensagens. Se durante a depuração ou teste você precisar descobrir o que é enviado ao SNS, você pode criar uma fila SQS, inscrevê-la no tópico desejado e ler a fila.

Teste automatizado de microsserviços no Docker para integração contínua

Gateway de API

A maioria dos serviços não pode ser acessada diretamente pela Internet. O acesso é via API Gateway, que verifica os direitos de acesso. Este também é o nosso serviço e também existem testes.

Notificações em tempo real

O aplicativo usa Sinal Rpara mostrar notificações em tempo real ao usuário. Isso é implementado no serviço de notificação. É acessível diretamente da Internet e funciona com OAuth, porque se revelou impraticável construir suporte para soquetes da Web no Gateway, em comparação com a integração do OAuth e do serviço de notificação.

Abordagem de teste bem conhecida

Os testes unitários substituem coisas como o banco de dados por objetos simulados. Se um microsserviço, por exemplo, tentar criar um registro em uma tabela com uma chave estrangeira, e o registro referenciado por essa chave não existir, a solicitação não poderá ser executada. Os testes de unidade não conseguem detectar isso.

В artigo da Microsoft Propõe-se usar um banco de dados na memória e implementar objetos simulados.

O banco de dados na memória é um dos SGBDs suportados pelo Entity Framework. Ele foi criado especificamente para testes. Os dados nesse banco de dados são armazenados apenas até que o processo que os utiliza seja encerrado. Não requer a criação de tabelas e não verifica a integridade dos dados.

Os objetos simulados modelam a classe que estão substituindo apenas na medida em que o desenvolvedor do teste entende como ela funciona.

Como fazer com que o Postgres inicie e execute migrações automaticamente quando você executa um teste não está especificado no artigo da Microsoft. Minha solução faz isso e, além disso, não adiciona nenhum código específico para testes ao próprio microsserviço.

Vamos passar para a solução

Durante o processo de desenvolvimento, ficou claro que os testes unitários não eram suficientes para encontrar todos os problemas em tempo hábil, por isso decidiu-se abordar esta questão de um ângulo diferente.

Configurando um ambiente de teste

A primeira tarefa é implantar um ambiente de teste. Etapas necessárias para executar um microsserviço:

  • Configure o serviço em teste para o ambiente local, especifique os detalhes para conexão ao banco de dados e AWS nas variáveis ​​de ambiente;
  • Inicie o Postgres e execute a migração executando o Liquibase.
    Em SGBDs relacionais, antes de gravar dados no banco de dados, é necessário criar um esquema de dados, ou seja, tabelas. Ao atualizar uma aplicação, as tabelas devem ser trazidas para a forma utilizada pela nova versão e, preferencialmente, sem perda de dados. Isso é chamado de migração. A criação de tabelas em um banco de dados inicialmente vazio é um caso especial de migração. A migração pode ser integrada no próprio aplicativo. Tanto o .NET quanto o NodeJS possuem estruturas de migração. No nosso caso, por questões de segurança, os microsserviços são privados do direito de alterar o esquema de dados, e a migração é realizada através do Liquibase.
  • Inicie o Amazon LocalStack. Esta é uma implementação de serviços AWS para execução doméstica. Existe uma imagem pronta para LocalStack no Docker Hub.
  • Execute o script para criar as entidades necessárias no LocalStack. Os scripts de shell usam a AWS CLI.

Usado para testes no projeto Postman. Já existia antes, mas foi lançado manualmente e testado em um aplicativo já implantado no estande. Esta ferramenta permite fazer solicitações HTTP(S) arbitrárias e verificar se as respostas correspondem às expectativas. As consultas são combinadas em uma coleção e toda a coleção pode ser executada.

Teste automatizado de microsserviços no Docker para integração contínua

Como funciona o teste automático?

Durante o teste, tudo funciona no Docker: o serviço em teste, o Postgres, a ferramenta de migração e o Postman, ou melhor, sua versão de console - Newman.

Docker resolve vários problemas:

  • Independência da configuração do host;
  • Instalando dependências: o Docker baixa imagens do Docker Hub;
  • Retornando o sistema ao seu estado original: simplesmente removendo os recipientes.

Composição do Docker une contêineres em uma rede virtual, isolada da Internet, na qual os contêineres se encontram por nomes de domínio.

O teste é controlado por um script de shell. Para executar o teste no Windows usamos git-bash. Assim, um script é suficiente para Windows e Linux. Git e Docker são instalados por 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 as seguintes etapas:

  • Construindo imagens do Docker
    docker-compose build
  • Iniciando o banco de dados e LocalStack
    docker-compose up -d <контейнер>
  • Migração de banco de dados e preparação de LocalStack
    docker-compose run <контейнер>
  • Iniciando o serviço em teste
    docker-compose up -d <сервис>
  • Executando o teste (Newman)
  • Parando todos os contêineres
    docker-compose down
  • Publicação de resultados no Slack
    Temos um chat onde vão as mensagens com uma marca de seleção verde ou uma cruz vermelha e um link para o log.

As seguintes imagens Docker estão envolvidas nestas etapas:

  • O serviço que está sendo testado tem a mesma imagem da produção. A configuração para o teste é através de variáveis ​​de ambiente.
  • Para Postgres, Redis e LocalStack, são usadas imagens prontas do Docker Hub. Também existem imagens prontas para Liquibase e Newman. Construímos o nosso no esqueleto deles, adicionando nossos arquivos lá.
  • Para preparar o LocalStack, você usa uma imagem AWS CLI pronta e cria uma imagem contendo um script baseado nela.

Uso volumes, você não precisa criar uma imagem do Docker apenas para adicionar arquivos ao contêiner. No entanto, os volumes não são adequados para nosso ambiente porque as próprias tarefas de CI do Gitlab são executadas em contêineres. Você pode controlar o Docker a partir desse contêiner, mas os volumes montam apenas pastas do sistema host e não de outro contêiner.

Problemas que você pode encontrar

Aguardando a prontidão

Quando um contêiner com serviço está em execução, isso não significa que ele esteja pronto para aceitar conexões. Você deve esperar que a conexão continue.

Este problema às vezes é resolvido usando um script espere por isso.sh, que aguarda uma oportunidade para estabelecer uma conexão TCP. No entanto, LocalStack pode gerar um erro 502 Bad Gateway. Além disso, é composto por vários serviços e, se um deles estiver pronto, isso não diz nada sobre os outros.

Solução: scripts de provisionamento LocalStack que aguardam uma resposta 200 do SQS e do SNS.

Conflitos de tarefas paralelas

Vários testes podem ser executados simultaneamente no mesmo host Docker, portanto, os nomes dos contêineres e da rede devem ser exclusivos. Além disso, testes de diferentes ramos do mesmo serviço também podem ser executados simultaneamente, portanto não basta escrever seus nomes em cada arquivo composto.

Solução: o script define a variável COMPOSE_PROJECT_NAME com um valor exclusivo.

Recursos do Windows

Há uma série de coisas que quero destacar ao usar o Docker no Windows, pois essas experiências são importantes para entender por que ocorrem erros.

  1. Os scripts de shell em um contêiner devem ter terminações de linha do Linux.
    O símbolo shell CR é um erro de sintaxe. É difícil dizer pela mensagem de erro que esse é o caso. Ao editar esses scripts no Windows, você precisa de um editor de texto adequado. Além disso, o sistema de controle de versão deve ser configurado corretamente.

É assim que o git está configurado:

git config core.autocrlf input

  1. Git-bash emula pastas padrão do Linux e, ao chamar um arquivo exe (incluindo docker.exe), substitui caminhos absolutos do Linux por caminhos do Windows. No entanto, isso não faz sentido para caminhos que não estejam na máquina local (ou caminhos em um contêiner). Este comportamento não pode ser desativado.

Solução: adicione uma barra adicional ao início do caminho: //bin em vez de /bin. O Linux entende esses caminhos; para ele, várias barras são iguais a uma. Mas o git-bash não reconhece esses caminhos e não tenta convertê-los.

Saída de registro

Ao executar testes, gostaria de ver os logs de Newman e do serviço que está sendo testado. Como os eventos desses logs estão interligados, combiná-los em um console é muito mais conveniente do que dois arquivos separados. Newman lança via execução do docker-compose, e assim sua saída acaba no console. Resta apenas garantir que a saída do serviço também vá para lá.

A solução original era fazer docker-compose up sem bandeira -d, mas usando os recursos do shell, envie este processo para segundo plano:

docker-compose up <service> &

Isso funcionou até que foi necessário enviar logs do Docker para um serviço de terceiros. docker-compose up parou de enviar logs para o console. Contudo, a equipe trabalhou anexar docker.

Solução:

docker attach --no-stdin ${COMPOSE_PROJECT_NAME}_<сервис>_1 &

Conflito de identificador durante iterações de teste

Os testes são executados em diversas iterações. O banco de dados não foi limpo. Os registros no banco de dados possuem IDs exclusivos. Se anotarmos IDs específicos nas solicitações, teremos um conflito na segunda iteração.

Para evitá-lo, os IDs devem ser exclusivos ou todos os objetos criados pelo teste devem ser excluídos. Alguns objetos não podem ser excluídos devido a requisitos.

Solução: gere GUIDs usando scripts Postman.

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

Em seguida, use o símbolo na consulta {{myUUID}}, que será substituído pelo valor da variável.

Colaboração via LocalStack

Se o serviço que está sendo testado lê ou grava em uma fila SQS, para verificar isso, o próprio teste também deve funcionar com essa fila.

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

A API de serviços da AWS está documentada, permitindo que consultas sejam feitas sem um SDK.

Se um serviço grava em uma fila, nós o lemos e verificamos o conteúdo da mensagem.

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

Se o serviço precisar ler uma mensagem da fila, na etapa de teste anterior gravamos essa mensagem na fila.

Testando solicitações HTTP originadas do microsserviço em teste

Alguns serviços funcionam por HTTP com algo diferente da AWS, e alguns recursos da AWS não são implementados no LocalStack.

Solução: nestes casos pode ajudar Servidor simulado, que tem uma imagem pronta em Hub do Docker. As solicitações e respostas esperadas a elas são configuradas por uma solicitação HTTP. A API está documentada, por isso fazemos solicitações ao Postman.

Testando autenticação e autorização OAuth

Usamos OAuth e Tokens da Web JSON (JWT). O teste requer um provedor OAuth que possamos executar localmente.

Toda interação entre o serviço e o provedor OAuth se resume a duas solicitações: primeiro, a configuração é solicitada /.bem conhecido/configuração openide, em seguida, a chave pública (JWKS) é solicitada 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 é gerado uma vez e confirmado no Git.

Recursos de teste SignalR

Postman não funciona com websockets. Uma ferramenta especial foi criada para testar o SignalR.

Um cliente SignalR pode ser mais do que apenas um navegador. Existe uma biblioteca cliente para isso no .NET Core. O cliente, escrito em .NET Core, estabelece uma conexão, é autenticado e aguarda uma sequência específica de mensagens. Se uma mensagem inesperada for recebida ou a conexão for perdida, o cliente sai com o código 1. Se a última mensagem esperada for recebida, o cliente sai com o código 0.

Newman trabalha simultaneamente com o cliente. Vários clientes são lançados para verificar se as mensagens são entregues a todos que delas necessitam.

Teste automatizado de microsserviços no Docker para integração contínua

Para executar vários clientes use a opção --escala na linha de comando do docker-compose.

Antes de ser executado, o script Postman espera que todos os clientes estabeleçam conexões.
Já encontramos o problema de esperar por uma conexão. Mas havia servidores e aqui está o cliente. É necessária uma abordagem diferente.

Solução: o cliente no contêiner usa o mecanismo Exame de saúdepara informar o script no host sobre seu status. O cliente cria um arquivo em um caminho específico, por exemplo, /healthcheck, assim que a conexão é estabelecida. O script HealthCheck no arquivo docker é assim:

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

Equipe docker inspecionar Mostra o status normal, o status de integridade e o código de saída do contêiner.

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

A felicidade existe

Depois de superarmos as dificuldades descritas acima, tivemos um conjunto de testes de execução estável. Nos testes, cada serviço funciona como uma unidade única, interagindo com o banco de dados e o Amazon LocalStack.

Esses testes protegem uma equipe de mais de 30 desenvolvedores contra erros em um aplicativo com interação complexa de mais de 10 microsserviços com implantações frequentes.

Fonte: habr.com

Adicionar um comentário