Introduzindo testes automatizados em equipes pequenas

Publicado por

Publicado por

Publicado em

    Categorias:

    Desenvolvimento
    Qualidade

Introduzir testes automatizados para uma equipe pequena que ainda não está familiarizada com o processo pode ser desafiador por vários motivos. Este artigo apresenta a minha opinião a respeito de tais desafios, e como acredito ser eficiente abordar a adoção deste processo dado um contexto específico: uma equipe desenvolvendo uma aplicação web de propósito geral, não tendo experiência prévia com testes automatizados.

Por que os testes automatizados são uma boa ideia?

Um cenário comum para uma equipe pequena é trabalhar em um produto em estágio inicial que ainda está buscando a proposta de valor e as funcionalidades corretas a desenvolver, ou mesmo um sistema mais antigo, porém ainda em um cenário de baixo orçamento. O escopo do projeto muda com frequência e os feedbacks dos clientes podem alterar as prioridades do backlog da noite para o dia. Os processos de garantia de qualidade geralmente são limitados a diversas rotinas de testes manuais, antes e depois dos lançamentos, a fim de garantir que após as alterações o sistema ainda se comportará conforme esperado.

À medida que o produto evolui, a solução tecnológica começa a ficar mais difícil de manter (frequentemente mudanças em uma funcionalidade quebram outra) e os habituais testes manuais tornam-se mais difíceis, cansativos e propensos a falhas. Além disso, novos clientes - muitas vezes mais exigentes em qualidade do que os early adopters - passam a utilizar o produto, aumentando a quantidade de bugs encontrados e as expectativas por um sistema de maior qualidade. Neste cenário fica claro que aumentar as horas de trabalho em testes manuais antes dos lançamentos simplesmente não é escalável.

O uso de processos de testes automatizados fornece à equipe de desenvolvimento um método consistente e reproduzível de testar as funcionalidades da aplicação. Isso melhorará a qualidade do produto, garantindo que a) códigos novos estejam funcionando conforme o esperado e b) alterações não quebrem funcionalidades já existentes.

Contextualização sobre testes automatizados

É importante observar que implantar um ambiente de testes automatizados vai além da escrita dos scripts de teste, e aqui eu gostaria de ressaltar alguns fatores:

  • Os testes precisam ser eficazes na detecção de defeitos e bugs, evitando falsos negativos; um script de teste que não testa nada relevante sempre será bem-sucedido.
  • Os testes precisam ser reproduzíveis para garantir resultados consistentes e facilidade de uso nas diversas etapas do processo de desenvolvimento; os testes que dependem de fatores externos, como a hora local do sistema ou respostas de API de terceiros, levarão a resultados não determinísticos.
  • Os testes precisam ser eficientes, rodar rapidamente, e não podem travar os ambientes de trabalho dos desenvolvedores. Isso é especialmente importante para encorajar a adoção e uso regular das ferramentas de testes automatizados pela equipe.
    • 💡
      Em Extreme Programming Explained, Kent Beck sugere 10 minutos como meta para o tempo de testes e compilação, o que ele chama de “Ten-Minute Build".

Embora uma discussão mais profunda de cada um dos bullet-points possa ser relevante, aqui estão algumas dicas práticas a serem seguidas para melhorias:

  • Para aumentar a eficácia dos testes, a equipe deve aprender os princípios e boas práticas de testes automatizados, e sugiro a adoção da revisão de código para o código de produção e de testes.
  • Para garantir a repetibilidade dos testes, a equipe deve estar atenta às dependências externas e utilizar técnicas como mocking e test doubles para evitar que estas interfiram nos resultados dos testes de forma não determinística.
  • Para aumentar a eficiência dos testes, sugiro implementar um ambiente de Integração Contínua. Isso permite a execução paralela em máquinas na nuvem, mantendo os ambientes de trabalho dos desenvolvedores livres, além de promover a repetibilidade ao padronizar a execução dos teste em um ambiente centralizado.

Outro ponto importante é que os testes automatizados podem ser implementados usando diferentes abordagens e em diferentes camadas do seu software, resultando em diferentes tipos de cobertura, custos de desenvolvimento e de manutenção.

A convenção de nomenclatura para essas diferentes abordagens varia muito, mas para este artigo, vamos dividir nos 3 tipos abaixo:

notion image

Testes de UI ou End-to-End

A terminologia em torno do teste de End-to-End (E2E) pode ser confusa, pois o termo às vezes é intercambiável com o de teste de aceitação de API. Para os fins deste artigo, definiremos o teste E2E como referindo-se especificamente aos testes que envolvem a interface do usuário (UI).

Os testes E2E concentram-se em simular a experiência do usuário ao testar todo o sistema da perspectiva do mesmo, tudo isso por cima de uma plataforma simulada (por exemplo, um navegador da web), sem conhecimento da implementação interna. É importante observar que os testes E2E podem ser focados apenas no front-end, usando chamadas de API “mockadas”, ou podem testar todo o sistema executando os serviços de front-end e back-end simultaneamente.

A animação mostra exemplo de login funcionando em um aplicativo usando o Cypress como ferramenta para criar os testes E2E
A animação mostra exemplo de login funcionando em um aplicativo usando o Cypress como ferramenta para criar os testes E2E

As maiores preocupações em relação aos testes E2E são:

  1. Tempo de execução elevado. Os testes E2E têm um longo tempo de execução, podendo desacelerar o desenvolvimento e também limitar o número de testes que podem ser executados.
  1. Fragilidade. Os testes E2E frequentemente geram falsos positivos devido a fatores externos e podem acusar falhas mesmo quando o código está funcionando corretamente.
👉
Em resumo, o foco principal desses testes é garantir que as interações do usuário estejam funcionando conforme o desejado, levando em consideração a simulação mais próxima de um usuário real (por exemplo, executando em um navegador da web)

Testes de integração

Às vezes testes de integração e unitários são colocados juntos, mas para este artigo, consideraremos os testes “sociáveis” vs “solitários” como a divisão entre os testes unitários e de integração, conforme discutido pelo Martin Fowler em seu artigo UnitTest.

  • Os testes de integração são testes sociáveis (aqueles que dependem de outras unidades para funcionar corretamente)
  • Testes unitários são testes solitários (aqueles que ficam isolados de outras unidades no ambiente de testes).
notion image

Dito isso, os testes de integração são capazes de testar uma quantidade razoável de lógica de negócios - geralmente da perspectiva do usuário - e ainda mantendo um tempo de execução moderado. Vamos trazer alguns exemplos, dividindo entre Back-end e Front-end.

 

Testes de integração para Back-end

Para o desenvolvimento back-end, os testes de integração são responsáveis por testar um “fluxo de execução” completo. Para APIs RESTful, por exemplo, isso significa enviar uma requisição para um endpoint e validar que a resposta recebida é a esperada. Já no caso de uma API GraphQL, seria o envio de uma query ou mutation para um field específico, também validando a resposta recebida. A lógica se mantém para qualquer tipo de API web.

É importante observar que muitas vezes é necessário “mockar” certas partes do código, especialmente quando existem chamadas para API externas ou interações com bancos de dados. Isso é necessário para isolar o sistema que está sendo testado e garantir que os resultados do teste sejam determinísticos e confiáveis.

👉
Em resumo, o foco principal desses testes é garantir que a API está se comportando conforme o esperado do ponto de vista do usuário, testando como o sistema responde a entradas específicas.
 

Testes de integração para Front-end

Quando se trata de desenvolvimento front-end, os testes de integração também testam o "fluxo de execução" completo, mas com foco em observar o estado de uma estrutura DOM-like, em vez do tratamento de solicitações de API. Em resumo, as ferramentas de teste de integração de front-end simulam de perto como os navegadores são usados em um aplicativo real, mas exigem menos configuração, têm tempos de execução significativamente menores, além de gerarem menos falsos positivos do que os testes E2E. Uma ótima ferramenta para testes de integração de front-end web é a Testing Library, que oferece uma API para vários frameworks javascript. Esta ferramenta também defende um princípio de que “quanto mais seus testes se assemelharem à maneira como seu software é usado, mais confiança eles podem oferecer a você”, com o qual concordo plenamente. Exemplificando, um teste de aplicação web com Testing Library seria assim:

 
test('login flow', async () => {
  const container = getDOM()

  // Get form elements by their label text.
  const emailInput = getByLabelText(container, 'Email')
  const passInput = getByLabelText(container, 'Password')
  emailInput.value = 'myemail@test.com'
  passInput.value = '123456'

  // Get elements by their text, just like a real user does.
  getByText(container, 'Sign in').click()

  // Expects a signed in label to exist.
  await waitFor(() =>
    expect(queryByTestId(container, 'signed-in-label')).toBeTruthy(),
  )
})

Como você pode ver, o script de teste simula um comportamento comum de um usuário em um formulário de login, primeiro digitando o e-mail e a senha e, em seguida, clicando no botão “Sign In”.

👉
Em resumo, o foco principal desses testes é garantir que, após as interações do usuário com componentes específicos da UI, o estado da aplicação seja alterado conforme o esperado.

Testes unitários

São testes feitos para validar pequenas partes do sistema. Eles têm grandes vantagens em termos de desempenho (executar centenas deles geralmente leva segundos) e, dependendo da arquitetura do sistema, são baratos para construir e manter. A maior preocupação desta abordagem é que geralmente demanda que muitos testes sejam construídos para se alcançar uma cobertura razoável, e visto que eles levam em conta como o código está desenvolvido, é importante ter o conhecimento sobre como os módulos interagem (principalmente para mocking). Dito isso, o design do seu código de produção terá influência determinante na facilidade de construir e manter testes de unidade.

Os testes de unidade de front-end e back-end (para a definição de testes de unidade deste artigo) têm estruturas semelhantes e seu principal objetivo é garantir que pequenos pedaços de código de negócios funcionem conforme o esperado, utilizando muito de mocks para isolar o módulo testado.

👉
Em resumo, o foco principal dos testes de unidade é verificar o comportamento de pequenos pedaços isolados de código, em vez de testar as interações entre vários componentes ou a interface do usuário.

E finalmente, como traçar o caminho para implantar um ambiente de testes automatizados?

Depois de toda esta introdução sobre diferentes abordagens de teste, é importante observar que uma estratégia eficaz de testes automatizados deve ser composta de uma combinação de abordagens para maximizar seus pontos fortes e minimizar o custo de desenvolvimento e manutenção. Em vez de discutir amplamente um cenário ideal para todos os casos, compartilharei minha estratégia pessoal, assim como o racional, para incorporar processos de teste automatizados no contexto discutido anteriormente.

 
Etapas propostas entre perceber a importância dos Testes Automatizados e ser capaz de escrever um código de produção e scripts de teste melhores.
Etapas propostas entre perceber a importância dos Testes Automatizados e ser capaz de escrever um código de produção e scripts de teste melhores.

Para facilitar a transição de equipes novas para testes automatizados, eu recomendaria focar na criação de testes da perspectiva do usuário, por alguns motivos. Primeiro, esses testes não exigem uma compreensão profunda do código interno e são escritos como um conjunto de instruções do usuário, tornando-os fáceis de entender para os desenvolvedores e até mesmo para a equipe não técnica. Além disso, os testes na perspectiva do usuário são fáceis de implementar em um software já existente, com pouca ou nenhuma alteração necessária no código de produção.

É importante observar que essa abordagem pode incluir (mas não está limitada a) testes E2E. Conforme discutido acima, existem várias ferramentas disponíveis para criar testes de integração que permitem escrever scripts pela perspectiva do usuário, mantendo um nível razoável de desacoplamento da estrutura de código subjacente e evitando o comportamento frequentemente inconsistente dos testes E2E.

 
De fato, acredito que a melhor abordagem para o contexto discutido seja implementar primeiro um ambiente de testes de integração.
 

No final das contas creio que uma equipe que invistir, por exemplo, 100 horas de trabalho na construção de testes de integração, terminará com um ambiente de testes mais eficaz e confiável, se comparado à uma equipe que investir o mesmo esforço para implementar testes E2E.

Mas por que integração em vez de testes de unidade?

Como mencionado anteriormente, esta é uma opinião baseada no contexto de uma pequena equipe sem experiência prévia com testes automatizados. Especificamente para testes unitários, julgo que entender as práticas de mocking e projetar o código de produção de maneira desacoplada são pontos cruciais para que os testes unitários sejam eficazes e de bom custo benefício.

  • Considero uma suíte de testes unitários ineficaz se ela realizar testes superfíciais que não garantem a confiabilidade dos trechos de código que está testando. Problemas como “uma alteração em uma feature quebrando outra” são um indicador disso.
  • Considero uma suíte de testes unitários de baixo custo benefício se forem necessárias alterações significativas no código de produção apenas para construí-la, se o setup dos testes for complicado ou se for necessário usar mocks muito complexos.

Um código de produção projetado sem ter testes automatizados em mente provavelmente apresentará todas essas características.

A ideia não é priorizar um tipo de teste em detrimento de outro, até porque as diferentes camadas de testes são complementares e importantes. O ponto é que, na opinião deste autor, o processo de adoção de testes automatizados será mais suave se inicialmente o foco for na implementação de testes de integração construídos na perspectiva do usuário. Essa primeira fase da adoção pode levar várias semanas (4-16 semanas, eu diria), nas quais a equipe deve se concentrar em melhorar suas técnicas de escrita, aumentar a consistência, e diminuir o tempo de execução dos testes.

À medida que a equipe se torna mais proficiente em testes automatizados, um aumento de foco para testes unitários será mais natural, e inclusive a busca por tempos de execução mais baixos e menores taxas de falsos positivos deve levar o time para este caminho espontaneamente. Neste momento, aquilo que antes seria um desafio na adoção de testes automatizados passará a ser mais um de seus benefícios: a percepção de que um código mais limpo é também um código mais testável.

Ou seja, neste momento o time encontrará mais motivos para desenvolver um código de produção com menor acoplamento para que o processo de escrever testes automatizados seja mais eficiente e previsível. De fato, acredito que a estrutura do código de produção seja tão relevante para a construção de testes unitários eficazes, que gostaria de avaliar alguns princípios de desenvolvimento sob esta perspectiva. Vamos então utilizar os famosos 5 princípios SOLID (destinados a tornar projetos orientados a objetos mais flexíveis e manuteníveis) sob a ótica dos testes automatizados:

  1. Single Responsibility Principle: Este princípio afirma que uma classe deve ter apenas um motivo para mudar. Seguindo esse princípio, fica mais fácil testar uma classe isoladamente pois ela tem uma responsabilidade única.
  1. Open-Closed Principle: Este princípio afirma que uma classe deve ser aberta para extensão, mas fechada para modificação. Seguindo esse princípio, os casos de teste tendem a ser mais duráveis, pois menos modificações serão necessárias em testes já existentes quando novas funcionalidades forem adicionada.
  1. Liskov Substitution Principle: Este princípio afirma que os objetos de uma superclasse devem poder ser substituídos por objetos de uma subclasse, sem que a aplicação quebre. Seguindo este princípio, torna-se mais fácil escrever casos de teste para uma classe, uma vez que pode ser facilmente substituída por um mock ou um test double.
  1. Interface Segregation Principle: Esse princípio afirma que uma classe não deve ser forçada a implementar interfaces que não usa. Seguindo esse princípio, as suítes de teste tendem a ser menores e mais valiosas, pois testam apenas aquilo que de fato é relevante para a aplicação, além de possuírem menos dependências.
  1. Dependency Inversion Principle: Esse princípio afirma que módulos de alto nível não devem depender de módulos de baixo nível, e sim que ambos devem depender de abstrações. Seguindo esse princípio, fica mais fácil testar uma classe pois suas dependências podem ser facilmente substituídas por mocks, e a elaboração destes mocks é mais óbvia, visto que as abstrações já estarão definidas.

Em resumo, o SOLID promoverá módulos menores e menos propensas a mudanças, resultando em casos de teste mais simples e duráveis. Além disso, gostaria de destacar que o princípio de Inversão de Dependência fornece uma maneira muito prática de simular as fronteiras do módulo a ser testado, sendo uma estratégia excelente para a construção de testes unitários confiáveis e baratos.

💡
Embora o SOLID seja um conjunto de princípios bem conhecido para programação orientada a objetos, podemos extrapolar a ideia de limitar as responsabilidades de cada módulo e reduzir o acoplamento entre eles para outros paradigmas.

Conclusão

A implementação de processos de testes automatizados em equipes de tecnologia tem um impacto significativo na eficiência e na qualidade final do trabalho. É claro que será necessário aprender um conjunto novo de ferramentas, tanto para escrever quanto para executar os testes, mas certamente é um caminho que trará retornos em diferentes aspectos. E pensando neste “caminho” de adoção, este artigo levanta a opinião de que, se baseado em testes automatizados da perspectiva do usuário, tal trajetória será mais natural, ao mesmo tempo em que levantará informações valiosas sobre o comportamento do sistema. Depois de ter um ambiente de testes automatizados (ainda que em processo de maturidade) configurado, a equipe poderá aprimorar o código de produção, aumentando a eficiência da escrita de testes unitários que dependem de uma boa estrutura, como também resultando em uma produto final de maior qualidade e robustez (e clientes mais satisfeitos 😀).

Considerações extras

  • Conforme mencionado, as opiniões expressas neste artigo estão focadas no contexto de desenvolvimento de aplicações web. Outras plataformas podem ter requisitos, benefícios e limitações diferentes.
  • Neste artigo estamos focando nossos argumentos em testes automatizados funcionais. Existe um conjunto totalmente diferente de testes não funcionais (por exemplo, teste de carga, teste de penetração…) que são mantidos fora de nossa discussão.
  • O sucesso da adoção de testes automatizados depende não apenas da maturidade da equipe, mas também da disposição em adotar novos processos e fluxos de trabalho.
  • As equipes que usam uma abordagem de desenvolvimento ágil provavelmente se beneficiarão na implementação de novos processos, devido à natureza iterativa destas metodologias. Isso certamente se extende para a adoção do processo de testes automatizados.