Pensando em testes - Anti patterns - Introdução e o primeiro alvo: o "Faz tudo"

Pensando em testes - iniciando a apresentação de anti patterns. Começando pelo padrão mais comum. O famoso "faz tudo"!

Pensando em testes - Anti patterns - Introdução e o primeiro alvo: o "Faz tudo"
Foto de Pixabay: https://www.pexels.com/pt-br/foto/pilha-de-livros-cobertos-159751/

Talvez a melhor forma de aprender seja com os próprios erros. Por esta razão inclui nesta série uma seção chamada "Anti patterns", na qual vou mostrar alguns padrões de código aplicados aos testes que podem tornar sua vida bem mais difícil.

É importante antes de apresentar estes anti patterns entender que normalmente estes padrões surgem por boas razões. Num primeiro momento se mostram como soluções aceitáveis para o problema mas com o tempo se mostram como um... problema.

💡
Hei! Dependendo do contexto talvez não sejam anti patterns. Leve isto em consideração, tá?

Estrutura do anti pattern

Todos os anti patterns que apresentaremos nesta série serão dissecados nas seguintes partes:

  • O nome - para que possamos criar um vocabulário para o futuro.
  • O que o motivou - por que achamos que seria uma boa ideia.
  • Como reconhecer o padrão - detalhes em frases do dia a dia ou no código que os identificam.
  • Quando são úteis - ou seja, as situações nas quais o anti pattern é realmente a solução.
  • Soluções - possíveis soluções para o problema (quando existirem)

Faz tudo

One-man band; photo by Knox of Athol, Massachusetts, in 1865

O primeiro anti-pattern é o mais comum que conheço e é o resultado do problema que apontei no início desta série: as pessoas só se preocuparem em aplicar boas práticas no código a ser validado, não no teste em si.

Pensando em testes - Parte 1
Uma nova série de posts - Pensando em testes: como evitar que seus testes automatizados se tornem um problema.

Tudo surge aqui.

Como nasce o faz-tudo - motivação

Para ilustrar o famoso vamos a uma história envolvendo casos de testes integrados contra uma API. Você inicia com uma estrutura tal como a exposta no diagrama abaixo:

Há um único teste, que irá validar se é possível cadastrar os dados do usuário com sucesso nesta API se todas as condições necessárias estão satisfeitas. Em um segundo momento, você também vai escrever um teste que irá validar o comportamento da API quando os dados estão incorretos. E neste momento surge aquela oportunidade de melhoria, então seu código fica como o exemplo a seguir:

Você percebeu que para cada teste seria necessário autenticar um usuário para então poder realizar novas requisições para a sua API. Logo uma função privada que já te retorne os dados de autorização para chamadas subsequentes faz todo o sentido.

Um dos atributos do usuário que você cadastra no seu teste é um código de identificação. Você percebeu que daria pra ir um pouco além nesta engenharia, então evolui um pouco mais o teste.

Criou uma função chamada geraCodigo() que retorna uma string contendo um valor do tipo UUID (ou qualquer coisa randômica) apenas para facilitar a escrita dos testes. Legal: estas funções privadas vão montando uma caixinha de ferramentas que vai se mostrando útil com o tempo.

Algum tempo depois você precisa testar também o cadastro de Clientes, que usa outro perfil de usuário, o Analista. E ao observar que já há algumas funções úteis em TesteAPI Usuarios, resolve usar a herança da orientação a objetos para te ajudar.

É criada uma classe chamada BaseTest que será usada como base para todos os demais testes: nela estarão todas aquelas funções que serão úteis a mais de um teste do seu projeto. A ideia é excelente pois o objetivo é que com o tempo escrever estes testes vá se tornando mais simples pois há uma base maior de código que pode ser reaproveitada.

Conforme o tempo passa mais testes vão surgindo e sua equipe com muito boa intenção vai incluindo mais funções protegidas, funções estas que muitas vezes só serão úteis em um caso (o que vale é a intenção).

💡
Arquitetura mostra seu valor em situações como esta: novos membros da equipe tendem a reproduzir más práticas que estão no código que encontram.

Faz parte do papel do arquiteto evitar que isto ocorra.

Observe como em pouco tempo o conceito de responsabilidade única e mesmo de encapsulamento se perdeu:

  • Por que TesteAPIClientes precisa saber que existe um perfil chamado Analista?
  • Surgem funções na classe base que vão sendo compartilhadas por todos os testes, MESMO que não sejam necessárias.
    • Se policie! Pensou "isto pode ser útil no futuro" e só está sendo usado em um lugar? Aguarde a segunda ou terceira vez em que for reaproveitada para ser compartilhada.

BaseTest faz coisas demais: se você altera algo ali pode acidentalmente quebrar uma série de testes. Pior: além de fazer coisas demais, sabe demais. Não raro isto vai gerar código difícil de manter (funções protegidas que chamam privadas que chamam não sei quem) e pior: que aumentam a carga cognitiva de TODA a equipe.

E sabe quando piora? Quando você quer começar um processo de especialização e parte pra criar "SUB CLASSES DE BASETEST".

Como reconhecer o padrão

Há alguns comportamentos que são "batata" pra identificar este comportamento.

💡
"ser batata" - expressão de Minas Gerais que representa "estar certo" ou "é isto mesmo".
  • Toda classe de teste que você cria precisa extender BaseTest, mas você não se pergunta mais o por quê.
  • Você olha pra classe base do seu teste e vê uma infinidade de funções que não sabe pra que servem.
  • Você se pegou pensando em ajudar o time criando funções em BaseTest "que podem ser úteis no futuro" mas que só estão sendo usadas para o seu caso.

Como resolver

Tenho uma receitinha pra você que funciona bem demais:

  • Substitua herança por composição.
  • Se uma função só é usada em uma classe de testes, mova esta função para esta classe a a mantenha privada.

A mesma estrutura que tratamos neste exemplo refatorada pode ficar tal como no exemplo abaixo:

É verdade que o número de dependências entre as classes aumenta: você não tem mais aquela única dependência (BaseTest), TesteAPIClientes por exemplo usa duas outras classes utilitárias para funcionar. Mas sabe o que é bom aqui? O que é utilitário se torna acessório. E código com uma única responsabilidade: mais fácil de manter e evoluir.

Seu teste precisa de um usuário autenticado e também gerar códigos randômicos? Legal: basta importar as funcionalidades que você realmente precisa.

💡
Código acessório é importantíssimo para que tenhamos uma base de testes que possa ser evoluída no futuro.

E sabe o que recomendo? Que você escreva testes pra ela também.

TesteAPIClientes tem acesso à função autenticarGestor, que ele não usa, é verdade: mas por outro lado em UtilAuth podemos implementar tudo o que de fato será compartilhado por mais testes na plataforma (no futuro aqui vão surgir outros testes que precisarão de um Gestor - e aqui cabe também o conceito de cenários sobre o qual tratamos na primeira parte da série).

Quando é válido o "Faz Tudo"?

Na minha experiência apenas nas situações em que realmente todas as funções compartilhadas em BaseTest são usadas por todas as suas sub-classes. Tirando isto... é cilada.

Concluindo

Este é apenas o nosso primeiro anti-pattern. Tem vários! Se quiser acompanhar a série, clica no link abaixo.

Pensando em testes
Uma das grandes dificuldades no desenvolvimento de software é a escrita de testes e validações. Muitas equipes tem dificuldade em iniciar a escrita dos testes, mas sabe o que é ainda mais difícil? Manter toda esta base de código! Esta série de posts tem como objetivo te ajudar neste segundo

A série completa!

Mantido por itexto Consultoria