Particionando execução testes unitarios em dois agentes no DevOps Pipeline

Particionando execução testes unitarios em dois agentes no DevOps Pipeline
Aplicação mobile híbrida desenvolvida em cima do Cordova com ReactJs no front-End com mais de 2500 testes

Particionando execução testes unitarios em dois agentes no DevOps Pipeline

Aplicação mobile híbrida desenvolvida em cima do Cordova com ReactJs no front-End com mais de 2500 testes

Particionando execução testes unitarios em dois agentes no DevOps Pipeline
Aplicação mobile híbrida desenvolvida em cima do Cordova com ReactJs no front-End com mais de 2500 testes

Nossa aplicação mobile híbrida desenvolvida em cima do Cordova com ReactJs no front-End, é madura, já com mais de 5 anos de desenvolvimento com dezenas de features e que é usada por milhares de usuários pelo mundo.

Tem hoje mais de 2500 testes unitários, com uma cobertura de quase 90%, porém com esse grande número de testes fica cada vez mais difícil de executar todos os testes em cada PR.

Voce pode se perguntar, "- Mas porque fica mais difícil ?"

Contextualizando

Vamo lá, com o uso do Cordova para gerar aplicações tanto pra iOS quanto para Android, foi utilizado um outro framework para lidar com as peculiaridades da renderização dos componentes para cada plataforma.

O Onsenui, essa biblioteca nos tira a responsabilidade de saber como cada componentes deve se comportar e ser estilizado em cada plataforma específica, ela cria elementos HTML customizados para este fim (está parte é importante, será comentado mais à frente )

Em nossa stack de execução de testes usamos o Karma, o porque?

O Jest era uma ótima opção, porém esbarramos nos componentes do Onsenui 

Pois ele gerada "Custom Elements" ou seja, cria HTML específicas para a biblioteca tipo `<ons-list />`.

Assim o JsDom do Jest não consegue reconhecer esses Elementos Html com isso não consegue executar-los nos testes.

A própria lib do Onsenui recomenda o uso do Karma pois ele sobe instâncias do Chrome (navegador) por exemplo para rodar os testes encima de um navegador, assim conseguindo rodar qualquer elemento HTML sendo customizado ou não.


Outra coisa é que nossos testes são todos assíncronos, devido aos Sagas e disparos de Actions, nos testes temos "listeners" que esperam que o disparo de uma action dipare uma outra.

O Problema

Bom, vamos ao problema que começamos a enfrentar, quando rodávamos os testes em um pipeline em máquinas provisionadas, antes no Jenkins e hoje no AzureDevops, os testes com frequência quebravam por timeOut de espera de algum teste que acabavam não disparando suas actions.

Acreditamos que seria por limitações de gerenciamento de memória dessas maquinas, pois quando rodávamos na nossa máquina localmente sempre todos os testes passavam. Então tinhamos que re-executar os testes novamente, onde geralmente depois de 2 ou 3 tentativas obtiamos sucesso.

Com o crescente aumento de testes assíncronos cada vez mais teríamos q ter máquinas mais robustas provisionadas para rodar estes testes e nem sempre era possível, devido aos custos.

Quando migramos do Jenkins para o Azure Devops o problema se agravou ainda mais, pois as máquinas provisionadas eram bem limitadas ao rodar um pipeline em um PR (Pull Request).

A Solução

Então peguei esse filho (task) pra tratar, comecei estudando formas de particionar a execução de testes unitarios no pipeline do AzureDevops, na documentação encontrei uma seção que trata justamente disso.

Basicamente é usar um pequeno algoritimo que fatia sua lista de arquivos de testes e roda cada grupo de testes em “Agentes” (máquina provisionada) diferentes.

A documentação é bem clara e direta, vou tentar explicar conforme está lá (link

Basicamente temos duas variáveis de sistema (que podemos acessar no ambiente da execução do pipeline no devOps)

- `System.TotalJobsInPhase` (totalDeFatias ou totalDeAgentes)

- `System.JobPositionInPhase` (numeroDaFatia ou numeroDoAgenteAtual)

Onde o algoritimo irá fatiar da seguinte forma em um loop

- `[numeroDaFatia + totalDeFatias]`

- onde o `numeroDaFatia` será o index da lista inicial e será incrementada conforme acima, e para acessar o valor a lista de testes seria [index - 1], deixa eu mostrar:

echo "Total agents: $totalDeFatias"
echo "Agent number: $numeroDaFatia"
echo "Total test files: $testCount"

for (( i = $numeroDaFatia; i <= $testCount; ))
do
file = ${ tests[$i - 1] }

./node_modules/.bin/mocha run test file

i = $(( $i + $totalDeFatias ))
done

- Supondo que iremos dividir em 2 agentes, o primeiro agente o valor de `numeroDaFatia = 1`, entao o primeiro File seria na posicao "0", o proximo será na posição "2", e assim sucessivamente

- No agente 2, será, `numeroDaFatia = 2`, o primeiro File de teste será "1', no proximo será "3", e assim sucessivamente.

E por fim no pipeline basta setar a `strategy: parallel: 2`

jobs:
- job: ParallelTesting
strategy:
parallel: 2

Minha Solução

Agora vamos com minhas implementações e adaptações. Usando o exemplo citado anteriormente, usando o algoritmo que faz essa divisão dos arquivos de testes, no arquivo de setup de testes do Karma fiz:

let totalAgents = +process.env.TOTAL_AGENTS;
let agentNumber = +process.env.AGENT_NUMBER;

if (totalAgents === 0 || !totalAgents) totalAgents = 1;
if (!agentNumber) agentNumber = 1;

const srcContext = require.context('../src', true, /.test.jsx?$/); // Onde buscamos todos os arquivos de testes
const srcContextKeys = srcContext.keys();

srcContextKeys.forEach(() => {
const filePathSelected = srcContextKeys[agentNumber - 1];

if (filePathSelected) {
srcContext(filePathSelected);
}

agentNumber += totalAgents;
});


E no meu arquivo de pipeline:

jobs:
- job: UnitTestsJob
displayName: "Unit Tests"
strategy:
parallel: 2
...
steps:
- script: |
...

yarn test:ci

displayName: "Build and Start Tests"
env:
AGENT_NUMBER: $(System.JobPositionInPhase)
TOTAL_AGENTS: $(System.TotalJobsInPhase)


Então, o que vai acontecer aqui, no pipeline estou setando como variáveis de ambiente o AGENT_NUMBER e o TOTAL_AGENTS, para que eu possa acessar seus valores no arquivo de setup do Karma mostrado mais acima.

O TOTAL_AGENTS vai levar o valor setado em `parallel: 2` ou seja, o valor "2" e no AGENT_NUMBER, como o parallel esta como 2, então serão duas execuções em paralelo, em uma ele será de 1, e no outro será de valor igual a 2.

Com estas informações, olhando agora para o arquivo de setup do Karma recuperamos estes valores usando `process.env.`, fazemos umas checagens condicionais para quando não existirem estes valores, como no caso de rodar os testes localmente.

No próximo passo colhemos todos os arquivos de testes do projeto, e em seguida fazemos o "Loop" para efetivamente "fatiar" os testes, uma parte para o AGENT_NUMBER = 1 e a outra para o AGENT_NUMBER = 2.

Dentro do loop, começamos pegando o primeiro teste pelo "index", suponto que estamos na execução do AGENT = 1:

  • na primeira interação, fazemos `[agentNumber - 1]` que será igual a posição "0" da array.
  • checa condicionalmente se existe o arquivo e o executa.
  • e por fim, faz `agentNumber += totalAgents;`, que é o mesmo que `agentNumber = 1 + 2` passando o valor de `agentNumber` para igual a "3" na proxima interação.

Se seguir essa logica, verá que o proximo teste será na posição de index "2", pois `3 - 1 = 2` e assim sucessivamente.

E quando for o `AGENT = 2`:

  • na primeira interação, fazemos `[agentNumber - 1]`, ou seja, `2 - 1` que será igual a posição "1" da array.
  • checa condicionalmente se existe o arquivo e executa.
  • e por fim, faz `agentNumber += totalAgents;`, que é o mesmo que `agentNumber = 2 + 2` passando o valor de `agentNumber` para igual a "4" na proxima interação.

Seguindo a logica, teremos a posição de index "3", pois `4 - 1 = 3` e assim sucessivamente.

Seguindo essa abordagem, podemos dividir os testes ainda mais em varios agentes em paralelo apenas aterado o valor de `parallel` no pipeline.


Assim foi possível dividir e rodar em paralelo nosso pipeline de tests sem mais problemas e de quebra ganhando tempo de execução que era de 12min em média caiu para 6min.

Espero que possa ajudar outros times ou desenvolvedores a executar grande quantidade des testes em seus processos de CI/CDs.

Qualquer coisa esse é Twitter @elfiservice

  • Compartilhe esse post
  • Compartilhar no Facebook00
  • Compartilhar no Google Plus00
  • Compartilhar no Twitter

Olá, deixe seu comentário para Particionando execução testes unitarios em dois agentes no DevOps Pipeline

Enviando Comentário Fechar :/

Converse Comigo