TDD: testes Depois do Deploy?

Matheus Mesquita
b2w engineering
Published in
11 min readJan 13, 2021

Grandes projetos precisam de grandes decisões técnicas. Quando seus testes começam a te atrapalhar, o que fazer?

Há um tempo atrás, meu time decidiu remover todos os testes e2e do pipeline do nosso CI. Eles rodavam em um framework baseado no selenium e sua execução estava demorando muito tempo, diminuindo nossa agilidade na entrega e solução de bugs.

Esse projeto é o mais antigo do nosso time, onde mais de 10 desenvolvedores já codificaram, e muitos outros, possivelmente, irão continuar esse trabalho.

Estamos falando do núcleo de uma loja online, onde milhares de clientes fazem visitas diariamente. Esse projeto é a tela de pagamento, um local no qual erros fazem a Companhia perder dinheiro.

Nossa decisão de remover os testes e2e foi arriscada. Transferir a responsabilidade de testes para o nosso PO ou Líder estava criando uma dependência no quanto podemos confiar em um humano testando isso.

Lembrem-se, humanos falham.

Sem dúvida nós experimentamos isso na veia, mas em compensação, sem os testes desnecessários nós conseguimos entregar mais e diminuir a pressão do time para atacar outros débitos técnicos que o projeto tinha. Esse projeto foi criado em um tempo onde SPA era algo novo e AngularJS estava começando a ser algo grande, então acredite, tínhamos muitos.

Olhando para a nossa decisão, algumas questões foram levantadas:

  1. Quão importante são os testes e2e?
  2. Como nós podemos adicionar testes e2e ao nosso fluxo de trabalho sem que isso seja um fardo para nós?
  3. Qual ferramenta pode ser utilizada para manter a nossa suite de testes desacoplada do framework do projeto?

Essas serão as perguntas respondidas do longo do texto. Espero que gostem. :)

Quão importante são os testes e2e?

Definições

Antes de começar a discussão, é necessária a definição de alguns significados para todos estarem na mesma página.

Como falado por Simon Stewart nesse blog post, nomear os tipos de teste é um trabalho complicado, uma vez que a comunidade de software não possui uma definição clara para cada tipo de teste. Com certeza, você já ouviu falar de diferentes nomes para o mesmo conceito.

  • Testes end-to-end são aqueles que verificam que todas as partes de uma aplicação estão funcionando juntas, de ponta a ponta. Nessa categoria, nós testamos todos os aspectos de uma interação de usuário na nossa aplicação. Um exemplo de descrição seria: em uma página de login o usuário clica no campo e-mail, digita o e-mail, clica no campo de senha, digita sua senha, clica no botão de login, a página envia uma requisição para a API que responde com um status code 200.
  • Testes de UI são parecidos com os testes e2e (end-to-end), mas eles testam exclusivamente como os dados da API são mostrados na interface e como o usuário interage nela. Nós não usamos API's de verdade nesse teste, pois conseguimos usar dublês de teste (stubs e spies). Um exemplo desse seria: em uma página de login, após focar e inserir os valores nos respectivos campos, quando o usuário clicar no botão de login, nosso spy irá garantir que a requisição foi enviada e nosso stub irá prover a resposta com status code 200.

Com as definições postas à mesa, vamos olhar um velho conceito sobre testes.

A boa e velha Pirâmide de Testes

Testes e2e e UI normalmente dividem o lugar da Pirâmide de Testes, um bom lugar para começarmos a procurar a resposta da nossa primeira pergunta: quão importante são os testes e2e?

Apesar da pirâmide não mostrar todas as categorias de teste LITERALMENTE, ela resume os casos mais comuns em softwares tradicionais, suficiente para nós.

Como demonstrado acima, nossa pirâmide cria uma perfeita analogia sobre como nós devemos priorizar nossos testes. De baixo para cima, nós devemos testar as menores partes ou nossas unidades, que são mais isoladas e permitem um rápido feedback.

Necessitando menos esforço para serem escritos quando comparados aos testes de UI que, devido a sua natureza, são mais complexos, maiores e com uma execução mais demorada.

É fácil deduzir que, caso decidamos focar somente em testes unitários, nosso desenvolvimento será mais rápido, o que no mundo ágil tende a ser preferível ao invés da qualidade.

O pior cenário é quando nossa pirâmide faz um cross-over com Stranger Things e vira de cabeça pra baixo; quando esse anti-pattern acontece, nossa aplicação começa a nos atrapalhar. Testes de UI e e2e possuem um tempo de execução maior, fazendo com que nosso pipeline de CI/CD fique demorado, impactando a produtividade do time. E dependendo de como você implementou os testes, eles acabam sendo um ninho de falsos positivos, removendo toda a confiança que o seu time tinha neles quando começaram o desenvolvimento.

Testes de UI e e2e não são algo fácil. Perder seu controle pode acarretar um grande débito técnico no pior lugar possível. O lugar que supostamente deveria trazer segurança para a aplicação, os testes.

Infelizmente perdemos o controle e remover os testes foi mais útil que fazer a manutenção dos mesmos, pois o valor gerado era menor que o esforço exigido naquele momento no projeto.

Testes unitários são suficientes?

Com todos esses lados negativos, aparentemente temos a resposta sobre o topo da nossa pirâmide de testes, né? Vamos parar de escrever testes e2e/UI!!!

Problema resolvido?

Não! Todas as vezes que você achou que escrever testes unitários seria o suficiente, você transformou sua aplicação em um lugar não seguro :(

Como podemos adicionar testes e2e ao nosso fluxo de trabalho sem que isso seja um fardo?

Vamos olhar o problema a partir de uma outra perspectiva.

Quando estamos desenvolvendo software, nós devemos sempre mirar em criar sistemas ortogonais. Quando conseguimos deixar as partes da nossa aplicação desacopladas, isso nos traz segurança e previsibilidade. Considerando essa afirmação como um axioma do desenvolvimento de software, vamos reavaliar ambas alternativas que estão no topo da nossa pirâmide.

Testes End-to-End

Como comentado na área de definições, testes e2e irão validar nosso fluxo de ponta a ponta, incluindo as respostas das API's e passos complexos para testar algum cenário específico, tipo aqueles casos nos quais sua aplicação só começou a quebrar porque o usuário apertou tantas teclas e clicou em tantos lugares que você não sabia se ele estava tentando usar sua aplicação ou liberar um easter egg no Mortal Kombat.

Algo nesse parágrafo parece estranho pra você? Se você respondeu que sim, você está correto. Ter um teste de uma aplicação front-end verificando uma resposta real de API é acoplar sua asserção ao fato de API estar funcionando ou não.

No caso da API ficar indisponível por um motivo qualquer, seu teste irá falhar por um motivo que não diz respeito ao projeto atual. Para ser claro nesse cenário, sua interface iria quebrar durante o desenvolvimento, apesar do problema estar relacionado a uma aplicação que não é responsabilidade do seu time: a API.

Os testes de uma aplicação front-end não podem ser dependentes das aplicações back-end, apesar de ser extremamente importante o bom relacionamento dos dois em produção.

Para o propósito dos testes, é possível criar stubs para as respostas das API's e acreditar que o time responsável por elas irá entregar os dados no contrato correto, além de deixar a aplicação estável para uso em produção.

Olhando para essa perspectiva, os testes e2e não parecem ser uma boa alternativa para aplicações front-end modernas, já que a nossa camada de visualização está desacoplada da camada de dados, às vezes estando deployadas em ambientes ou nuvens diferentes.

Testes de UI

O que realmente importa para uma aplicação front-end é testar se o comportamento dos componentes está correto, se os dados são mostrados no lugar correto e se as ações estão sendo executadas quando o usuário interage na interface.

Testes de UI são direcionados exatamente para esses pontos. Se criarmos stubs e editarmos o estado da nossa aplicação, conseguimos forçar cenários onde uma sessão normal de usuário (como as que implementaríamos nos testes e2e) demoraria segundos ou até mesmo minutos para serem replicadas, devido as interações com as API's.

Então quais partes da nossa aplicação devemos testar?

A quantidade correta de testes

É difícil escolher quais cenários devemos testar. Lembre-se do princípio de pareto! Provavelmente vamos usar 80% do nosso esforço para escrever os primeiros 20% dos nossos testes e os 20% de esforço restante não serão suficientes para escrever os 80% do que falta nos testes.

Então, além de ser contraprodutivo, aumentar o a cobertura de testes não garantirá que sua aplicação está segura.

Vale mencionar que não estou defendendo que 20% de cobertura é suficiente para sua aplicação. Isso irá depender de cada projeto e somente você pode decidir a porcentagem correta de cobertura que sua aplicação necessita. Só mantenha em mente que números acima de 40% são altos e talvez você devesse reavaliar o que você está testando.

Para a tarefa de decidir quais partes devemos testar, eu trouxe algumas sugestões.

Evite duplicação

Apesar de existirem aplicações onde um grande número de interações é necessário, devemos enxergar se isso na verdade é um problema em como nosso teste foi escrito.

Esse caso pode ocorrer facilmente quando duplicamos testes. Por exemplo, dois testes repetem todos os passos necessários para realizar o login antes de interagir em alguma página interna da sua aplicação.

Lembre-se que o princípio DRY também deve ser aplicado para os nossos testes. Se você tem um comportamento que precisa ser realizado em muitos cenários diferentes, crie um modulo separado para esse teste e use-o nos locais necessários.

Além disso, quando você duplica seus testes, acaba deixando uma surpresa desagradável para um futuro colega de trabalho ou até mesmo para você na hora de dar manutenção àquele código, pois toda vez que o comportamento daquela parte mudar, você precisará alterar em todos os locais onde ocorreu a duplicação.

Se é difícil de ser replicado, provavelmente é difícil para seu usuário também

Não existe nenhuma razão para um cenário onde somente 1% dos seus usuários experienciam quebrar o pipeline de CI/CD da sua aplicação. E é claro que você deseja 100% de cobertura de testes. Sendo assim, qual a melhor forma de garantir uma aplicação totalmente segura que não seja testando ela integralmente?

Não! Você não deveria estar focando em uma métrica de cobertura de testes causando uma cobertura incidental na sua aplicação. 100% de cobertura deveria ser um benefício de bons testes ou do desenvolvimento de software no ciclo TDD, no qual você só escreve implementação o suficiente para passar o caso de teste que foi escrito.

Com isso em mente, devemos focar somente nas funcionalidades essenciais do nosso produto. No caso de uma loja virtual por exemplo, um usuário DEVE conseguir finalizar uma compra.

Uma nova esperança

Ok, correr atrás dos 100% de cobertura é um anti-pattern, mas não seria maravilhoso se algo nos avisasse dos nossos erros antes que alguém aparecesse na nossa baia ou nos mandasse um e-mail informando que algo que estava funcionando anteriormente parou de funcionar após a última feature lançada?

Claro que isso não aconteceria em um software bem planejado, mas, às vezes, você não participou dos primórdios do projeto e as decisões tomadas no passado por você ou pelos seus colegas de trabalho culminaram em uma situação em que uma alteração localizada em um módulo específico causa um efeito colateral do outro lado da aplicação.

Analisando a causa raiz desse problema, iremos esbarrar no débito técnico, que possui suas formas de ser combatido. Como minha experiência trabalhando com software me mostrou, até nas vezes que conseguimos atacar o débito técnico, continuamos encontrando bugs que foram corrigidos no passado, voltando depois de uma alteração que não foi revisada corretamente.

Isso ocorreu porque não corrigimos o problema corretamente ou alguém simplesmente “descorrigiu” a correção?

Eu diria que nenhuma das duas opções. Estaríamos novamente olhando para o lugar errado ao buscar um culpado. Isso não deixará nosso projeto melhor ou corrigir o bug, só trará novos problemas e vai deteriorar o relacionamento entre os membros do time ou de outros times na companhia.

Testes de UI como parte do seu ciclo de desenvolvimento

Keep your friends close but your enemies closer — Michael Corleone

Nós aprendemos nos tópicos passados que testar todos os aspectos da nossa UI pode ser danoso o suficiente para criarmos paranoias sobre os nossos testes, além de ser contraprodutivo, aumenta o tempo de entrega e desmotiva nosso trabalho diário ao interromper nosso estado de flow.

Porém, simplesmente parar de escrever testes irá remover qualquer segurança que tínhamos na nossa aplicação, nos fazendo entregar código de menor qualidade e reduzindo a confiança que a empresa tem no nosso produto, com bugs continuamente aparecendo em todo novo deploy que fizermos.

Estou propondo que é importante mirar nos 100% de coverage, dividindo testes essenciais de testes não-essenciais. Locales errados não deveriam parar sua pipeline de CI, mas botões importantes não sendo renderizados corretamente deveriam.

Após dividir seus testes entre esses dois grupos, você pode tornar os testes essenciais obrigatórios para o deploy, mas os testes não-essenciais podem ser executados após o deploy.

A razão por trás disso é simples. Caso seus testes não-essenciais demorem mais de 30 minutos para serem executados, no pior dos cenários, somos avisados que nossa aplicação tem um problema 30 minutos depois do último deploy ter sido executado, permitindo um rollback rápido sem precisar que algum usuário reporte o problema.

Após você corrigir um bug reportado, lembre-se de criar um novo teste de UI para prevenir que ele aconteça novamente. Não pare de desenvolver funcionalidades novas para escrever testes de cenários complicados.

Isso acontecerá de forma progressiva seguindo a regra do escoteiro: sempre deixe o código que você encontrou melhor do que quando o achou. Fazendo isso, seu futuro você, seus colegas de trabalho, clientes e stakeholders ficarão agradecidos e você estará permitindo entregar "código não testado" sem causar muitos danos ou diminuir o ritmo de trabalho do seu time.

O que vem por aí…

No próximo post, eu vou comentar qual ferramenta nós podemos utilizar para manter nossos fluxos de negócio desacoplados das implementações. Também explicarei como podemos fazer isso utilizando o Gitlab CI, que é uma ótima ferramenta open-source utilizada na B2W para criar nossos pipelines de CI/CD.

Espero que tenham gostado, foi um prazer escrever para o blog da B2W sobre um case de tecnologia que tivemos aqui na nossa equipe de checkout front. Caso tenha ficado alguma dúvida, critica ou simplesmente desejar falar um "oi", estarei feliz em respondê-lo na seção de comentários.

Se você quer ser notificado sobre o próximo post e outros artigos incríveis que o time de engenharia da B2W anda compartilhando, siga o nosso Medium e as redes sociais.

Abraços e até a próxima!

Referências

https://en.wikipedia.org/wiki/Human_error
https://martinfowler.com/articles/practical-test-pyramid.html#UiTests
https://testing.googleblog.com/2010/12/test-sizes.html
https://dzone.com/articles/should-you-aim-for-100-percent-test-coverage
https://betterexplained.com/articles/understanding-the-pareto-principle-the-8020-rule/
https://jasonrudolph.com/blog/2008/06/17/testing-anti-patterns-incidental-coverage/
https://en.wikipedia.org/wiki/Test-driven_development
https://en.wikipedia.org/wiki/Don't_repeat_yourself
https://dev.to/caiorcferreira/using-technical-debt-as-your-next-tool-1bp6
https://medium.com/@biratkirat/step-8-the-boy-scout-rule-robert-c-martin-uncle-bob-9ac839778385
https://www.youtube.com/watch?v=DfHJDLoGInM
https://about.gitlab.com/product/continuous-integration/

Se você busca uma oportunidade de desenvolvimento, trabalhando com inovação em um negócio de alto impacto, acesse o portal B2W Carreiras! Nele, você consegue acessar todas as vagas disponíveis. Venha fazer parte do nosso time!

--

--