Este artigo será interessante tanto para testadores quanto para desenvolvedores, mas é mais voltado para automatizadores que enfrentaram o problema de configurar o GitLab CI/CD para a realização de testes de integração em condições de escassez de recursos de infraestrutura e/ou na ausência de uma plataforma de orquestração de contêineres. Vou mostrar como configurar a implantação de ambientes de teste usando o docker compose em um único runner do GitLab shell, garantindo que a implantação de vários ambientes não interfira nos serviços em execução entre si.
Conteúdo
Premissas
Na minha experiência, frequentemente precisei "curar" testes de integração em projetos. E muitas vezes, o primeiro e mais significativo problema é o pipeline CI, no qual o teste de integração do serviço(s) em desenvolvimento ocorre no ambiente dev/stage. Isso trouxe muitos problemas: Devido a falhas em um ou outro serviço durante o teste de integração, o contorno de teste pode ser corrompido por dados inválidos. Já houve casos em que o envio de uma solicitação com formato JSON corrompido travava o serviço, levando o ambiente a um estado completamente inoperante.
- O desempenho do contorno de teste diminui com o aumento dos dados de teste. Acredito que descrever um exemplo de limpeza/rollback de BD não faz sentido. Na minha prática, nunca encontrei um projeto onde esse procedimento corresse suavemente.
- O risco de prejudicar o funcionamento do contorno de teste ao testar as configurações gerais do sistema. Por exemplo, user/group/password/application policy.
- Os dados de teste dos testes automatizados dificultam a vida dos testadores manuais.
- Alguém pode dizer que bons testes automatizados devem limpar os dados após a execução. Tenho argumentos contrários:
Ambientes dinâmicos são muito convenientes de usar.
- Nem todos os objetos podem ser removidos do sistema através da API. Por exemplo, o chamado para deletar um objeto não foi implementado, pois contraria a lógica de negócios.
- Ao criar um objeto através da API, pode ser gerada uma enorme quantidade de metadados, que são difíceis de remover.
- Se os testes têm dependência entre si, o processo de limpeza de dados após a execução dos testes se transforma em uma dor de cabeça.
- Chamadas adicionais (e, na minha opinião, injustificadas) à API.
- Chamadas adicionais (e, na minha opinião, não justificadas) à API.
- E o argumento principal: quando os dados de teste começam a ser limpos diretamente do BD. Isso se transforma em um verdadeiro circo de PK/FK! Dos desenvolvedores, ouve-se: "Eu só adicionei/removi/renomeei uma tabela, por que 100500 testes de integração falharam?"
Na minha opinião, a solução mais otimizada é um ambiente dinâmico.
- Muita gente usa docker-compose para iniciar ambientes de teste, mas poucos o utilizam durante os testes de integração em CI/CD. E aqui não estou considerando kubernetes, swarm e outras plataformas de orquestração de contêineres. Nem toda empresa as tem. Seria bom se o docker-compose.yml fosse universal.
- Se temos nosso próprio runner de QA, como podemos garantir que os serviços iniciados através do docker-compose não interfiram uns com os outros?
- Como coletar logs dos serviços testados?
- Como limpar o runner?
Eu tenho meu próprio runner GitLab para meus projetos e eu enfrentei essas questões durante o desenvolvimento para . E, mais especificamente, ao executar testes de integração. A partir daqui, vamos resolver essas questões com exemplos deste projeto.
GitLab Shell Runner
Para o runner, recomendo uma VM Linux com 4 vCPU, 4 GB de RAM e 50 GB de HDD.
Na vastidão da internet, há muita informação sobre como configurar o gitlab-runner, então, de forma breve:
- Acesse a máquina via SSH
Se você tiver menos de 8 GB de RAM, recomendo , para que o OOM killer não venha e mate nossas tarefas por falta de RAM. Isso pode acontecer quando mais de 5 tarefas são executadas ao mesmo tempo. As tarefas passarão mais devagar, mas de forma estável.
Exemplo com OOM killer
Se nos logs da tarefa você ver
bash: line 82: 26474 Killed, basta executar no runnersudo dmesg | grep 26474[26474] 1002 26474 1061935 123806 339 0 0 java Fora da memória: Matar o processo 26474 (java) com pontuação 127 ou sacrificar o filho Matou o processo 26474 (java) total-vm:4247740kB, anon-rss:495224kB, file-rss:0kB, shmem-rss:0kBE se a imagem parecer mais ou menos assim, adicione swap ou aumente a RAM.
- Instalando , , , faça.
- Adicione o usuário
gitlab-runnerao grupodockersudo groupadd docker sudo usermod -aG docker gitlab-runner - gitlab-runner.
Abra para edição
/etc/gitlab-runner/config.tomle adicioneconcurrent=20 [[runners]] request_concurrency = 10Isso permitirá que tarefas paralelas sejam executadas em um único runner. Para mais detalhes, leia .
Se você tiver uma máquina mais poderosa, como 8 vCPU, 16 GB de RAM, esses números podem ser aumentados em pelo menos duas vezes. Mas tudo depende do que exatamente será executado neste runner e em que quantidade.
Isso é suficiente.
Preparação do docker-compose.yml
A principal tarefa é criar um docker-compose.yml universal que os desenvolvedores/testadores possam usar tanto localmente quanto no CI pipeline.
Primeiro, criamos nomes exclusivos para os serviços no CI. Uma das variáveis exclusivas no GitLab CI é a variável CI_JOB_ID. Se definir container_name com o valor "service-${CI_JOB_ID:-local}", então no caso:
- se
CI_JOB_IDnão estiver definida nas variáveis de ambiente,
o nome do serviço seráservice-local - se
CI_JOB_IDdefinida nas variáveis de ambiente (por exemplo, 123),
o nome do serviço seráservice-123
Em segundo lugar, criamos uma rede comum para os serviços em execução. Isso nos proporciona isolamento a nível de rede ao executar vários ambientes de teste.
networks:
default:
external:
name: service-network-${CI_JOB_ID:-local}Esse é o primeiro passo para o sucesso =)
Exemplo do meu docker-compose.yml com comentários
version: "3"
# Para o funcionamento correto do web (php) e fmt,
# os containers precisam ter um conteúdo executável compartilhado.
# Neste caso, é o diretório /var/www/testrail
volumes:
static-content:
# Isolamos o ambiente a nível de rede
networks:
default:
external:
name: testrail-network-${CI_JOB_ID:-local}
services:
db:
image: mysql:5.7.22
# Cada container_name contém ${CI_JOB_ID:-local}
container_name: "testrail-mysql-${CI_JOB_ID:-local}"
environment:
MYSQL_HOST: db
MYSQL_DATABASE: mydb
MYSQL_ROOT_PASSWORD: 1234
SKIP_GRANT_TABLES: 1
SKIP_NETWORKING: 1
SERVICE_TAGS: dev
SERVICE_NAME: mysql
networks:
- default
migration:
image: registry.gitlab.com/touchbit/image/testrail/migration:latest
container_name: "testrail-migration-${CI_JOB_ID:-local}"
links:
- db
depends_on:
- db
networks:
- default
fpm:
image: registry.gitlab.com/touchbit/image/testrail/fpm:latest
container_name: "testrail-fpm-${CI_JOB_ID:-local}"
volumes:
- static-content:/var/www/testrail
links:
- db
networks:
- default
web:
image: registry.gitlab.com/touchbit/image/testrail/web:latest
container_name: "testrail-web-${CI_JOB_ID:-local}"
# Se as variáveis TR_HTTP_PORT ou TR_HTTPS_PORT não estiverem definidas,
# o serviço será levantado nas portas 80 e 443, respectivamente.
ports:
- ${TR_HTTP_PORT:-80}:80
- ${TR_HTTPS_PORT:-443}:443
volumes:
- static-content:/var/www/testrail
links:
- db
- fpm
networks:
- defaultExemplo de execução local
docker-compose -f docker-compose.yml up -d
Iniciando testrail-mysql-local ... concluído
Iniciando testrail-migration-local ... concluído
Iniciando testrail-fpm-local ... concluído
Recriando testrail-web-local ... concluídoMas nem tudo é tão simples ao executar no CI.
Preparação do Makefile
Eu uso Makefile, pois é bastante conveniente tanto para gerenciamento local do ambiente quanto no CI. A seguir, comentários em linha
# У меня в проектах все вспомогательные вещи лежат в директории `.indirect`,
# в том числе и `docker-compose.yml`
# Использовать bash с опцией pipefail
# pipefail - фейлит выполнение пайпа, если команда выполнилась с ошибкой
SHELL=/bin/bash -o pipefail
# Останавливаем контейнеры и удаляем сеть
docker-kill:
docker-compose -f $${CI_JOB_ID:-.indirect}/docker-compose.yml kill
docker network rm network-$${CI_JOB_ID:-testrail} || true
# Предварительно выполняем docker-kill
docker-up: docker-kill
# Создаем сеть для окружения
docker network create network-$${CI_JOB_ID:-testrail}
# Забираем последние образы из docker-registry
docker-compose -f $${CI_JOB_ID:-.indirect}/docker-compose.yml pull
# Запускаем окружение
# force-recreate - принудительное пересоздание контейнеров
# renew-anon-volumes - не использовать volumes предыдущих контейнеров
docker-compose -f $${CI_JOB_ID:-.indirect}/docker-compose.yml up --force-recreate --renew-anon-volumes -d
# Ну и, на всякий случай, вывести что там у нас в принципе запущено на машинке
docker ps
# Коллектим логи сервисов
docker-logs:
mkdir ./logs || true
docker logs testrail-web-$${CI_JOB_ID:-local} >& logs/testrail-web.log
docker logs testrail-fpm-$${CI_JOB_ID:-local} >& logs/testrail-fpm.log
docker logs testrail-migration-$${CI_JOB_ID:-local} >& logs/testrail-migration.log
docker logs testrail-mysql-$${CI_JOB_ID:-local} >& logs/testrail-mysql.log
# Очистка раннера
docker-clean:
@echo Останавливаем все testrail-контейнеры
docker kill $$(docker ps --filter=name=testrail -q) || true
@echo Очистка докер контейнеров
docker rm -f $$(docker ps -a -f --filter=name=testrail status=exited -q) || true
@echo Очистка dangling образов
docker rmi -f $$(docker images -f "dangling=true" -q) || true
@echo Очистка testrail образов
docker rmi -f $$(docker images --filter=reference='registry.gitlab.com/touchbit/image/testrail/*' -q) || true
@echo Очистка всех неиспользуемых volume
docker volume rm -f $$(docker volume ls -q) || true
@echo Очистка всех testrail сетей
docker network rm $(docker network ls --filter=name=testrail -q) || true
docker ps
Verificamos
make docker-up
$ make docker-up
docker-compose -f ${CI_JOB_ID:-.indirect}\/docker-compose.yml kill
Killing testrail-web-local ... concluído
Killing testrail-fpm-local ... concluído
Killing testrail-mysql-local ... concluído
docker network rm network-${CI_JOB_ID:-testrail} || true
network-testrail
docker network create network-${CI_JOB_ID:-testrail}
d2ec063324081c8bbc1b08fd92242c2ea59d70cf4025fab8efcbc5c6360f083f
docker-compose -f ${CI_JOB_ID:-.indirect}\/docker-compose.yml pull
Pulling db ... concluído
Pulling migration ... concluído
Pulling fpm ... concluído
Pulling web ... concluído
docker-compose -f ${CI_JOB_ID:-.indirect}\/docker-compose.yml up --force-recreate --renew-anon-volumes -d
Recreating testrail-mysql-local ... concluído
Recreating testrail-fpm-local ... concluído
Recreating testrail-migration-local ... concluído
Recreating testrail-web-local ... concluído
docker ps
CONTAINER ID PORTS NAMES
a845d3cb0e5a 0.0.0.0:80->80/tcp, 0.0.0.0:443->443/tcp testrail-web-local
19d8ef001398 9000/tcp testrail-fpm-local
e28840a2369c 3306/tcp, 33060/tcp testrail-migration-local
0e7900c23f37 3306/tcp testrail-mysql-local
make docker-logs
$ make docker-logs
mkdir .\/logs || true
mkdir: não é possível criar o diretório ‘.\/logs’: O diretório já existe
docker logs testrail-web-${CI_JOB_ID:-local} >& logs\/testrail-web.log
docker logs testrail-fpm-${CI_JOB_ID:-local} >& logs\/testrail-fpm.log
docker logs testrail-migration-${CI_JOB_ID:-local} >& logs\/testrail-migration.log
docker logs testrail-mysql-${CI_JOB_ID:-local} >& logs\/testrail-mysql.log
Preparação do .gitlab-ci.yml
Execução de testes de integração
Integração:
estágio: teste
tags:
- my-shell-runner
before_script:
# Autenticando no registro
- docker login -u gitlab-ci-token -p ${CI_JOB_TOKEN} ${CI_REGISTRY}
# Gerando TR_HTTP_PORT e TR_HTTPS_PORT pseudo-únicos
- export TR_HTTP_PORT=$(shuf -i10000-60000 -n1)
- export TR_HTTPS_PORT=$(shuf -i10000-60000 -n1)
# criando diretório com o ID da tarefa
- mkdir ${CI_JOB_ID}
# copiando nosso docker-compose.yml para o diretório criado
# para que o contexto seja diferente para cada tarefa
- cp .indirect\/docker-compose.yml ${CI_JOB_ID}\/docker-compose.yml
script:
# levantando nosso ambiente
- make docker-up
# executando testes com jar executável (é assim que faço)
- java -jar itest.jar --http-port ${TR_HTTP_PORT} --https-port ${TR_HTTPS_PORT}
# ou no contêiner
- docker run --network=testrail-network-${CI_JOB_ID:-local} --rm itest
after_script:
# coletando logs
- make docker-logs
# parando o ambiente
- make docker-kill
artifacts:
# salvando logs
when: always
paths:
- logs
expire_in: 30 diasComo resultado da execução dessa tarefa, o diretório logs nos artefatos conterá os logs dos serviços e testes. Isso é muito conveniente em caso de erros. Cada teste, em paralelo, escreve seu próprio log, mas falarei sobre isso separadamente.

Limpeza do runner
A tarefa será executada apenas conforme agendado.
stages:
- clean
- build
- test
Clean runner:
stage: clean
only:
- schedules
tags:
- my-shell-runner
script:
- make docker-cleanEm seguida, vamos ao nosso projeto no GitLab -> CI/CD -> Schedules -> New Schedule e adicionamos um novo agendamento
Resultado
Executamos 4 tarefas no GitLab CI
Nos logs da última tarefa com os testes de integração, vemos contêineres de diferentes tarefas.
ID DO CONTÊINER NOME
c6b76f9135ed testrail-web-204645172
01d303262d8e testrail-fpm-204645172
2cdab1edbf6a testrail-migration-204645172
826aaf7c0a29 testrail-mysql-204645172
6dbb3fae0322 testrail-web-204645084
3540f8d448ce testrail-fpm-204645084
70fea72aa10d testrail-mysql-204645084
d8aa24b2892d testrail-web-204644881
6d4ccd910fad testrail-fpm-204644881
685d8023a3ec testrail-mysql-204644881
1cdfc692003a testrail-web-204644793
6f26dfb2683e testrail-fpm-204644793
029e16b26201 testrail-mysql-204644793
c10443222ac6 testrail-web-204567103
04339229397e testrail-fpm-204567103
6ae0accab28d testrail-mysql-204567103
b66b60d79e43 testrail-web-204553690
033b1f46afa9 testrail-fpm-204553690
a8879c5ef941 testrail-mysql-204553690
069954ba6010 testrail-web-204553539
ed6b17d911a5 testrail-fpm-204553539
1a1eed057ea0 testrail-mysql-204553539Log mais detalhado
$ docker login -u gitlab-ci-token -p ${CI_JOB_TOKEN} ${CI_REGISTRY}
AVISO! Usar --password via CLI é inseguro. Utilize --password-stdin.
AVISO! Sua senha será armazenada sem criptografia em /home/gitlab-runner/.docker/config.json.
Configure um auxiliar de credenciais para remover este aviso. Veja
https://docs.docker.com/engine/reference/commandline/login/#credentials-store
Login realizado com sucesso
$ export TR_HTTP_PORT=$(shuf -i10000-60000 -n1)
$ export TR_HTTPS_PORT=$(shuf -i10000-60000 -n1)
$ mkdir ${CI_JOB_ID}
$ cp .indirect/docker-compose.yml ${CI_JOB_ID}/docker-compose.yml
$ make docker-up
docker-compose -f ${CI_JOB_ID:-.indirect}/docker-compose.yml kill
docker network rm testrail-network-${CI_JOB_ID:-local} || true
Erro: Rede não encontrada: testrail-network-204645172
docker network create testrail-network-${CI_JOB_ID:-local}
0a59552b4464b8ab484de6ae5054f3d5752902910bacb0a7b5eca698766d0331
docker-compose -f ${CI_JOB_ID:-.indirect}/docker-compose.yml pull
Baixando web ... concluído
Baixando fpm ... concluído
Baixando migration ... concluído
Baixando db ... concluído
docker-compose -f ${CI_JOB_ID:-.indirect}/docker-compose.yml up --force-recreate --renew-anon-volumes -d
Criando volume "204645172_static-content" com driver padrão
Criando testrail-mysql-204645172 ...
Criando testrail-mysql-204645172 ... concluído
Criando testrail-migration-204645172 ... concluído
Criando testrail-fpm-204645172 ... concluído
Criando testrail-web-204645172 ... concluído
docker ps
ID DO CONTÊINER IMAGEM COMANDO CRIADO STATUS PORTAS NOMES
c6b76f9135ed registry.gitlab.com/touchbit/image/testrail/web:latest "nginx -g 'daemon of…" há 13 segundos Em execução há 1 segundo 0.0.0.0:51148->80/tcp, 0.0.0.0:25426->443/tcp testrail-web-204645172
01d303262d8e registry.gitlab.com/touchbit/image/testrail/fpm:latest "docker-php-entrypoi…" há 16 segundos Em execução há 13 segundos 9000/tcp testrail-fpm-204645172
2cdab1edbf6a registry.gitlab.com/touchbit/image/testrail/migration:latest "docker-entrypoint.s…" há 16 segundos Em execução há 13 segundos 3306/tcp, 33060/tcp testrail-migration-204645172
826aaf7c0a29 mysql:5.7.22 "docker-entrypoint.s…" há 18 segundos Em execução há 16 segundos 3306/tcp testrail-mysql-204645172
6dbb3fae0322 registry.gitlab.com/touchbit/image/testrail/web:latest "nginx -g 'daemon of…" há 36 segundos Em execução há 22 segundos 0.0.0.0:44202->80/tcp, 0.0.0.0:20151->443/tcp testrail-web-204645084
3540f8d448ce registry.gitlab.com/touchbit/image/testrail/fpm:latest "docker-php-entrypoi…" há 38 segundos Em execução há 35 segundos 9000/tcp testrail-fpm-204645084
70fea72aa10d mysql:5.7.22 "docker-entrypoint.s…" há 40 segundos Em execução há 37 segundos 3306/tcp testrail-mysql-204645084
d8aa24b2892d registry.gitlab.com/touchbit/image/testrail/web:latest "nginx -g 'daemon of…" há cerca de um minuto Em execução há 53 segundos 0.0.0.0:31103->80/tcp, 0.0.0.0:43872->443/tcp testrail-web-204644881
6d4ccd910fad registry.gitlab.com/touchbit/image/testrail/fpm:latest "docker-php-entrypoi…" há cerca de um minuto Em execução há cerca de um minuto 9000/tcp testrail-fpm-204644881
685d8023a3ec mysql:5.7.22 "docker-entrypoint.s…" há cerca de um minuto Em execução há cerca de um minuto 3306/tcp testrail-mysql-204644881
1cdfc692003a registry.gitlab.com/touchbit/image/testrail/web:latest "nginx -g 'daemon of…" há cerca de um minuto Em execução há cerca de um minuto 0.0.0.0:44752->80/tcp, 0.0.0.0:23540->443/tcp testrail-web-204644793
6f26dfb2683e registry.gitlab.com/touchbit/image/testrail/fpm:latest "docker-php-entrypoi…" há cerca de um minuto Em execução há cerca de um minuto 9000/tcp testrail-fpm-204644793
029e16b26201 mysql:5.7.22 "docker-entrypoint.s…" há cerca de um minuto Em execução há cerca de um minuto 3306/tcp testrail-mysql-204644793
c10443222ac6 registry.gitlab.com/touchbit/image/testrail/web:latest "nginx -g 'daemon of…" há 5 horas Em execução há 5 horas 0.0.0.0:57123->80/tcp, 0.0.0.0:31657->443/tcp testrail-web-204567103
04339229397e registry.gitlab.com/touchbit/image/testrail/fpm:latest "docker-php-entrypoi…" há 5 horas Em execução há 5 horas 9000/tcp testrail-fpm-204567103
6ae0accab28d mysql:5.7.22 "docker-entrypoint.s…" há 5 horas Em execução há 5 horas 3306/tcp testrail-mysql-204567103
b66b60d79e43 registry.gitlab.com/touchbit/image/testrail/web:latest "nginx -g 'daemon of…" há 5 horas Em execução há 5 horas 0.0.0.0:56321->80/tcp, 0.0.0.0:58749->443/tcp testrail-web-204553690
033b1f46afa9 registry.gitlab.com/touchbit/image/testrail/fpm:latest "docker-php-entrypoi…" há 5 horas Em execução há 5 horas 9000/tcp testrail-fpm-204553690
a8879c5ef941 mysql:5.7.22 "docker-entrypoint.s…" há 5 horas Em execução há 5 horas 3306/tcp testrail-mysql-204553690
069954ba6010 registry.gitlab.com/touchbit/image/testrail/web:latest "nginx -g 'daemon of…" há 5 horas Em execução há 5 horas 0.0.0.0:32869->80/tcp, 0.0.0.0:16066->443/tcp testrail-web-204553539
ed6b17d911a5 registry.gitlab.com/touchbit/image/testrail/fpm:latest "docker-php-entrypoi…" há 5 horas Em execução há 5 horas 9000/tcp testrail-fpm-204553539
1a1eed057ea0 mysql:5.7.22 "docker-entrypoint.s…" há 5 horas Em execução há 5 horas 3306/tcp testrail-mysql-204553539Todas as tarefas foram concluídas com sucesso
Os artefatos da tarefa contêm logs dos serviços e testes
Parece que tudo está em ordem, mas existe um detalhe. O pipeline pode ser cancelado manualmente durante a execução dos testes de integração, e nesse caso, os contêineres em execução não serão parados. De vez em quando, é necessário limpar o runner. Infelizmente, a tarefa de aprimoramento no GitLab CE ainda está pendente
Mas nós adicionamos a execução da tarefa de acordo com um cronograma, e ninguém nos impede de executá-la manualmente.
Vamos ao nosso projeto -> CI/CD -> Agendas e executamos a tarefa Limpar runner
No total:
- Temos um shell runner.
- Não há conflitos entre as tarefas e o ambiente.
- Temos a execução paralela de tarefas com testes de integração.
- É possível executar os testes de integração tanto localmente quanto no contêiner.
- Os logs dos serviços e testes são coletados e anexados à tarefa do pipeline.
- Há a possibilidade de limpar o runner de imagens Docker antigas.
O tempo de configuração é de aproximadamente 2 horas.
E é isso. Agradeço feedback.
Fonte: habr.com
