Arquitetura Front-End: Testes
Estou começando uma série de artigos sobre arquitetura de projetos, especialmente Front-End. Os posts terão ênfase em explicar em “português”, sem tanto código. Talvez, em outro momento, a parte prática seja demonstrada em uma série de vídeos. Postarei a partir de uma lista de diversos assuntos, desde pastas e arquivos, até versionamento, integração contínua e DevOps, provavelmente fora de ordem, no fim agrupando em uma lista ordenada. Por enquanto, escolhi começar por Testes. Deixem suas sugestões nos comentários :)
Antes de começar, é importante lembrar que existe o universo dos Formatters e Linters, mas prefiro deixá-los para um artigo sobre padrões, onde podemos falar das escolhas que são a razão dos linters existirem: reforçar padrões, seja por convenção, segurança ou qualquer motivo técnico.
Vamos falar sobre:
- Unitários
- Integração
- End-to-end
- Regressão.
Cada um está estruturado com:
- O que é
- Quando usar
- Pontos positivos
- Pontos negativos
- Ferramentas
Unitário
O que é
Como o nome diz, se refere às unidades. Pela palavra, significa que é algo que não pode ser dividido, já que é a menor unidade possível. No sentido de software, significa que são funções que fazem uma só coisa. Testes unitários são responsáveis por atestar que tais funções (ou unidades) estão se comportando da forma devida. Como estamos falando de Front-End, essas funções são escritas em Javascript ou qualquer linguagem que compile para o mesmo.
Usando o processo TDD ou não, geralmente ao programar tais funções, dificilmente o desenvolvedor leva em consideração todos os cenários possíveis. Escrever testes unitários ajuda a preparar o seu código para as situações fora do “ótimo”. Além disso, garantem que durante o tempo de vida do projeto, caso algo saia do formato ou da arquitetura estabelecida, os “little guys” (testes unitários) vão reclamar antes que você perceba tais erros. Ao escrevê-los, você está criando um exército de robôs com vida própria, cuja única missão é ficar observando as suas funções e garantindo que elas estão corretas.
Um exemplo, no caso de uma função que formata datas, seria garantir os outputs, para praticamente todo tipo de input. O que acontece ao usar uma data em diferentes formatos, datas inválidas, e assim por diante, inclusive como a função lida com erros e exceções.
Quando usar
- O projeto possui quantidade considerável de lógica
- Existem muitas funções que são reutilizadas em vários lugares
- Possui regra de negócio ou código crucial para o sucesso de fluxos de negócio
- É um produto ou projeto com vida longa
- É um projeto open-source com contribuidores
Obs: Como saber o que é código de lógica e o que não é? Lógica, na maioria das vezes é puro Javascript, ou seja, se você está acessando o DOM, trocando classes ou animando, isso já é a consequência da lógica, é um efeito colateral de uma decisão. Geralmente funções lógicas são compostas de condicionais (if-else, switch) e loops.
Pontos positivos
- Bugs são encontrados mais rápido
- Erros de run-time são tratados preventivamente
- Mais confiança nos deploys
- Facilita o refactor (reescrever funções mantendo os mesmos inputs e outputs)
Pontos negativos
- Mais tempo de desenvolvimento
- Mais tempo de ambientação com o projeto
- Dificulta a reescrita. Ao contrário do refactor, se uma função ou módulo foi mal planejado e precisar ser refeito, o teste precisará ser reescrito também
Ferramentas
Cobertura
Um ponto um pouco controverso. Ao usar testes unitários como sinônimo de mais estabilidade e segurança, é muito comum o compromisso ou exigência quanto a uma determinada “cobertura”, geralmente medida em percentual. A princípio quanto mais dos seus arquivos Javascript, funções, linhas, condições, enfim, cada operação, é coberta por testes, melhor. Mas existe “testar demais”. Um bom meio termo é escolher manualmente arquivos críticos, com regra de negócio ou lógica, e medir a cobertura dos mesmos, ao invés de todos os arquivos em absoluto.
Integração
O que é
Existe uma infinidade de gifs na internet exemplificando a diferença entre testes unitários e de integração. Basicamente significa testar unidades em conjunto. Enquanto nos testes unitários muitas vezes é necessário mockar (simular dados localmente, removendo a necessidade de integrações externas) parâmetros, dados e dependências, no teste de integração se utiliza tanto quanto possível as “unidades reais”.
Um exemplo, ainda no caso das datas. Provavelmente você teria, pelo menos, uma função para formatar datas e outra para formatar tempo (horário). Um simples teste de integração seria verificar que, mesmo a formatação de data e tempo funcionando isoladamente, o que aconteceria por exemplo se por acaso o formato de data e tempo forem diferentes? O output será data no formato de um país e hora no formato de outro, ou isso é uma exceção?
Quando usar
A palavra “integração” nesse contexto significa muita coisa. O objetivo desses testes é basicamente suprir a questão que fica no ar, “como duas unidades testadas unitariamente funcionam em conjunto?”. Porém, nesse caso, unidade pode ter vários significados:
- Testar duas ou mais funções, quando uma é usada pela outra e no teste unitário foi solucionado com mock
- Testar funções ou módulos que usam dependências externas (como pacotes NPM) e no teste unitário foi solucionado com mock
- Conectar APIs ou serviços externos em testes que anteriormente estavam com mock
- Em arquiteturas de microservice, testar funções ou módulos de repositórios diferentes que funcionem em conjunto
Pontos positivos
- Confere ainda mais segurança no funcionamento da aplicação, principalmente como validação antes de fazer deploy
- Avisa rapidamente quaisquer falhas por consequência de refactor, seja de código próprio ou de terceiros, que talvez só fossem pegos em produção
Pontos negativos
- Dependendo da stack e arquitetura do projeto, pode demandar tempo considerável de setup, inclusive provisionamento de instâncias na nuvem, setup e teardown de serviços
- Pode ser lento, aumentando consideravelmente o tempo de builds
Ferramentas
As mesmas dos testes unitários, mas dependendo da arquitetura do projeto você pode precisar rodar um browser para acessar funcionalidades específicas.
End-to-end
O que é
Como o nome diz, “de ponta a ponta”. Estamos falando de “aplicações”, que possuem dados, lógica e interface, portanto testes “end-to-end” devem englobar os três. São focados na interface como caminho para testar automaticamente comportamentos, interações e fluxos, que contenham ou não, dados e lógica.
Um exemplo, seria automatizar um teste de interface em que o usuário abre um calendário e escolhe datas, e em outra coluna escolhe horários, atestando que as opções corretas são mostradas, mensagens de sucesso e erro aparecem e assim por diante.
Quando usar
Sempre que você ou sua equipe se ver realizando dezenas de vezes o mesmo teste de interface e houver tempo para programar testes automatizados. O maior fator que define se é o caso ou não, é quanto tempo tais testes continuarão sendo feitos, ou quanto tempo de projeto. Se a resposta for pelo menos alguns meses, com certeza é o caso.
Pontos positivos
- Diminui consideravelmente o tempo de teste manual
- Se configurado corretamente, facilita encontrar divergências cross-browser
Pontos negativos
- Demanda tempo considerável para programar os testes
- Drivers, browsers headless e diferentes navegadores tem comportamento bem instável com testes automatizados, levando a demasiadas exceções que devem ser ignoradas manualmente
Ferramentas
- Nightwatch
- Webdriver
- CasperJS
- Selenium
- Conectar com: Browserstack
- Conectar com: AWS Device Farm
Regressão
O que é
Todos os testes, unitários, de integração e end-to-end, tem muito mais valia quando acompanhados no passar do tempo. É possível configurá-los para usar um log que mostre de forma clara o que tem tido êxito ou falhado. Se você estiver em um projeto bem estruturado, é possível que existam profissionais de QA dedicados, nesse caso provavelmente fazendo testes manuais exploratórios também, e anotando o resultado dos testes com o passar do tempo.
Enquanto os usos mencionados acima se focam principalmente na camada de dados e lógica de uma aplicação, existe outro uso, talvez um dos mais benéficos para projetos de grande escala. Por exemplo, observar automaticamente durante o tempo de vida de um produto ou projeto, as diferenças de interface, usando referências passadas. Isso é extremamente útil em um projeto com centenas ou milhares de páginas, onde para atestar que tudo está dentro dos conformes, levaria diversos dias ou semanas se feito manualmente. Por isso é válido usar testes de regressão.
Quando usar
Em softwares grandes onde existe um design system bem utilizado, ou seja, um sistema de componentes com regras consistentes de design. Em tais sistemas, é possível inferir através de scripts automatizados que mudanças grosseiras no design provavelmente são uma quebra no design system. Além disso, é possível ter um número grande demais de páginas para seres humanos darem conta de lembrar, principalmente considerando múltiplos releases e linhas do tempo.
Pontos positivos
- Ajuda a garantir que novos releases estão em conformidade com os anteriores que foram bem sucedidos.
Pontos negativos
- Requer testes end-to-end para ser efetivo.
- Exige tempo considerável de configuração e setup.
- Dependendo da sua matriz de compatibilidade pode não remover a necessidade de teste manual excessivo.
- Ainda não existe solução perfeita, lidar com responsividade, proporcionalidade de threshold e screenshots de todas as interações possíveis, são problemáticas.
Ferramentas
- PhantomCSS
- ResembleJS
- WebdriverCSS
- Percy ($)
- BackstopJS
- Wraith by BBC
- Ghost Inspector ($)
- CrossBrowserTesting ($)
Obs: Além dos testes mencionados acima, existem ainda ferramentas de teste específicas para uso com um framework, por exemplo Protractor do Angular e Enzyme para o React.
Testes são praticamente um outro mundo e existem vários novos desafios. É um assunto bem antigo e popular em desenvolvimento de software, principalmente no Back-End, mas é recente no Front, especialmente no Brasil. Cada vez mais as empresas solicitam esse conhecimento, entretanto ainda é bem raro! É uma boa forma de se diferenciar no mercado.
Ainda temos pouco conteúdo em português, se você tem interesse e não sabe por onde começar, me mande uma mensagem no Linkedin, ficarei feliz em ajudar com referências de conteúdo.