Como implantar uma cultura de testes onde não há testes

Cléber Zavadniak
clebertech
Published in
9 min readDec 24, 2020
Photo by Birmingham Museums Trust on Unsplash

Recentemente foi-me perguntado como eu faria para implantar uma cultura de testes automatizados em uma equipe que já desenvolve um produto tem algum tempo e, até agora, não tem nenhum teste automatizado.

Desafio interessante, não? E já adianto que o segredo para conseguir sucesso nisso reside em duas palavras: disciplina e flexibilidade

Você também pode ler este artigo lá no MyNotes, onde foi originalmente publicado:

Etapas

O plano de implantação segue duas etapas:

  1. nenhum bug é considerado como corrigido se não há um teste para provar e
  2. nenhuma nova funcionalidade é aceita sem testes.

Bugs e testes

A não ser que estejamos falando de uma equipe excepcionalmente qualificada e que apesar disso não tem uma cultura de automatizar testes, eu duvido que esse projeto não tenha uma grande fila de bugs a serem corrigidos. Logo, da parte da equipe de desenvolvimento, é necessário disciplina para que aquilo que estava sendo feito não se repita, que a própria falta de testes automatizados, que acaba sendo a causa da maioria dos bugs, não se perpetue durante o processo de correção de bugs.

E da diretoria é necessário flexibilidade, especialmente com relação aos prazos. Não se pode exigir que o desenvolvedor de software faça muito mais com a mesma quantidade de tempo. E é bem comum, inclusive, as equipes não escreverem testes justamente por pressa — geralmente misturada com algum desespero.

Algumas vezes será necessário reforçar ou pelo menos treinar melhor as equipes que lidam diretamente com os usuários do software para que o imediatismo não tome conta de tudo e seja barrado antes que cause dano ao produto final.

Ademais, há casos em que a situação já está tão deteriorada que a melhor decisão pode ser perder um cliente e receber o dano imediato do que tentar manter-se “ileso” e, no médio prazo, acabar queimando a equipe inteira e, consequentemente, tendo muito mais custos com o próprio produto.

Procedimento

Mas não é apenas com discurso que se resolvem as coisas. É necessário adotar um procedimento adequado.

A resolução de bugs passa a ter também dois passos integrados diretamente no processo de desenvolvimento, que são:

  1. escrever o teste que reproduz o bug e
  2. fazer o teste passar.

Faço questão de enfatizar a palavra “integrados” porque estas duas etapas devem, necessariamente, integrar-se no processo já estabelecido (considerando que haja algum processo estabelecido).

O que quero dizer? Que o primeiro passo deve sempre ser commitado no repositório.

Repito: o teste que falha deve ir para o galho-mestre do repositório de código. Ou qualquer que seja o galho que esteja sendo usado (development, talvez).

Testes que falham de propósito

“Mas… mas.. como?”, você pode estar se perguntando. Ao que lhe respondo: a maioria dos sistemas de testes automatizados permitem que você marque o teste como fail, ou seja, que você indique explicitamente que aquele teste, embora estando no repositório em um galho que não é de funcionalidade (chamado feature branch), irá, sim, falhar, e que isso é okay.

Isso tem dois propósitos: o primeiro é simples, que é dividir a tarefa de correção em duas partes que, inclusive, colaboram de maneira bem equilibrada para a resolução do problema. Obrigando-se a escrever o teste unitário, o desenvolvedor obriga-se também a entender melhor a situação em que o problema acontece e passa a ter, dessa forma, um entendimento melhor da coisa toda.

E então, num segundo momento, poderá concentrar-se na resolução.

Ou não! Pois a presença de um teste unitário facilita muito que outro desenvolvedor também possa fazer a correção. Metade do caminho já está andado, afinal! E isso é excelente para a equipe, porque tal granularização controlada das tarefas faz com que imprevistos pessoais, por exemplo, ou mesmo o surgimento de tarefas mais urgentes para quem começou a correção sejam problemas mais facilmente contornáveis.

O segundo propósito é “oficializar” essa primeira metade do caminho. O trabalho de programação, afinal, só é validado quando as alterações no código são integradas no repositório. Antes disso, tudo é teoria. Teoricamente o problema pode estar corrigido, mas se o código não vai para o galho adequado, então na prática a situação é outra (qual seja: não está nada “corrigido”).

Resolução

Estando o teste-que-falha devidamente integrado, é hora de resolvê-lo. Esse trabalho pode ser feito pelo mesmo desenvolvedor que escreveu o teste, mas também pode ser feito, como comentei acima, por ainda outra pessoa, o que pode ser interessante numa série de situações, como quando teste e resolução apresentam níveis de dificuldade diferentes ou mesmo caso algum desenvolvedor tenha um talento reconhecido (e gosto) na escrita de bons testes.

Passando o teste-que-agora-já-não-falha, pode-se considerar o bug, finalmente, corrigido.

Flexibilidade

Como mencionei, não adianta cobrar que os programadores façam mais dando-lhes exatamente o mesmo tempo. Implantar um novo processo demanda tempo, disciplina e paciência de todos os envolvidos. É o custo da melhoria que vem depois, que é, basicamente, ter o novo processo implantado e funcionando azeitado.

Ao mesmo tempo, é preciso admitir que nem sempre é por pura “má vontade” da diretoria que os prazos são curtos. Lidamos com o mercado, afinal, e as coisas não são sempre como planejamos. Nesse caso, sugiro que ambos os lados estejam dispostos a fazer sacrifícios. A diretoria permite que algum cliente fique brabo, mesmo. E a equipe de desenvolvimento faz um “mutirão”, inclusive consumindo umas horas-extras, para dar o empurrão inicial nesse novo processo.

Não é o ideal, não é gostoso, não é o desejado, mas é o que é possível fazer. Se todos querem colher os benefícios, me parece o caso de todos ajudarem também no custo.

O perigo das “métricas”

Números nunca tem significado absoluto. Estamos falando justamente de desenvolvimento de software, afinal, e programadores trabalham justamente com o tipo de ferramenta (código) que permite mascarar a realidade, seja isso proposital ou não.

“No último mês tivemos 25% menos registros de bugs” bem poderia ser uma das “frases ditas antes de a empresa morrer”, porque no fim do dia não tem em si mesmo um valor absoluto. O número de reports pode ter sido menor porque os clientes simplesmente desistiram. Ou porque o uso do software é sazonal. Ou porque um bug gigante surgiu e “eclipsou” todos os outros que ainda serão descobertos. Ou qualquer outra coisa que não seja “temos, com absoluta certeza, uma diminuição de 25% na quantidade de bugs no sistema*.

Muitos podem torcer o nariz para o que vou dizer, mas trust your feeling! Confie nas suas tripas. Números são uma ferramenta útil, mas é aquele conhecimento tácito de quem está em campo, de quem está ali no front, batalhando no dia-a-dia que realmente pode dar um vislumbre mais significativo da realidade.

Então use os números a seu favor, sim, mas também mantenha-se conectado às pessoas. É lindo dizer que se é uma “data-driven company”, mas se isso é em detrimento de ser uma “people-driven company”, recomendo repensar sua estratégia.

(Ou não. Sois livres.)

Novas funcionalidades

Não sou acólito da Igreja do TDD e tampouco vou dizer aqui que a equipe pode replicar no desenvolvimento de novas funcionalidades o exato procedimento da correção de bugs, porque não é assim que a coisa funciona.

É muito belo dizer “escreva primeiro os testes”, mas na prática implementar algo novo implementando primeiro testes unitários porque muitas vezes simplesmente nem nós mesmos sabemos imediatamente qual é o conjunto completo de inputs e outputs ou mesmo qual a melhor forma de apresentar o processo numa API palatável, o que faz com que escrever testes unitários nesse momento de experimentação acabe consumindo tempo demais com coisas que talvez acabem nem indo para produção realmente.

Também é belo imaginar que todos os detalhes podem ser debatidos e destrinchados durante as reuniões de grooming, mas esta é outra falácia. Não é à toa que “metodologias” mais recentes e que não estão sujeitas ao “politicamente correto do mundo do software”, como Shape Up! descrevam uma fase de figuring things out como parte inerente do trabalho em si.

Todavia, o período de experimentação e descoberta não cobre o todo e uma regra precisa ser mantida: nada entra no galho-mestre sem testes automatizados.

Repare que não usei o termo “unitários”, porque bem pode ser que a equipe decida que é mais vantagem focar primeiro em testes de integração. Fazer o desenvolvedor sentar a bunda na cadeira e digitar testes unitários cobrindo uma porção de casos que talvez nunca cheguem a realmente acontecer em campo pode não ser interessante em todos os casos e a equipe, que imagina-se que ainda tenha um mínimo de autonomia, pode decidir que um bom conjunto de testes de integração é suficiente.

Mas ainda é necessário ter testes automatizados. Se os testes de integração não podem ser rodados na esteira de CI, por qualquer motivo que seja, então eles não contam.

Ou seja: os testes

  1. tem que existir e
  2. necessariamente devem ser automatizados.

Enquanto essas duas condições não forem ambas satisfeitas, a equipe não deve permitir que o novo código seja integrado (a não ser que seja boilerplate conhecido, claro, e arquivos de tradução, conforme as Diretrizes de Desenvolvimento, capítulo 1).

Mas como impedir

Estabelecendo também uma política de code review. Um conjunto de regras que pode ser usado é o seguinte:

  1. Ninguém mescla (ou “mergeia”) o próprio código;
  2. O primeiro revisor deve aprovar (usando o Approve da ferramenta em uso ou simplesmente comentando com um “joinha”);
  3. O segundo revisor, aprovando, deve mesclar.

E contanto que não aconteça de dois revisores deixarem código sem testes passar, tudo estará bem.

Em algumas equipes pode ser necessário estabelecer que “júnior revisa, mas não avalia”. Não é o ideal mas, enfim, cada equipe é um caso particular. Novamente: sois livres.

Mas o resumo desse ponto é: disciplina. Não somente para impedir que código não-testado entre, mas também para ajudar quem está desenvolvendo a escrever os testes, se for o caso. A equipe sempre deve, afinal, “trabalhar em equipe”. É redundante, não? Então. Deveria ser óbvio.

Testes bons e testes ruins

Um sinal de que o teste é bom é que ele evita regressão. Regressão é um conceito simples: se um bug foi corrigido ontem e volta a aparecer hoje, então a qualidade do código regrediu. Da mesma forma, se ontem algo estava funcionando e hoje não está mais, a qualidade do código, novamente, regrediu.

Ou seja: os testes devem garantir que durante o ciclo de vida do software não seja possível alguém, acidentalmente, quebrar o que já está funcionando e deixar isso passar batido. Se alguém estraga uma funcionalidade, é a suíte de testes quem garante que isso não prosseguirá esteira adentro.

E um segundo sinal muito importante é que o teste prevê malícia ou idiotice. Se é um endpoint exposto ao mundo, pode ser interessante testar se o software irá se comportar adequadamente caso seja enviado um payload grotescamente maior do que normalmente esperado: se o endpoint recebe uma pequena string JSON, experimente enviar um grande livro para ver o que acontece.

Ou, se o endpoint é autenticado, sempre experimente operar sobre ele estando desautenticado. Ou tente mexer nas coisas dos outros. Lembre-se: insegurança é um bug. E o propósito dos testes é justamente manter o código livre de bugs.

Flexibilidade

Alterar o processo e ter a disciplina de não deixar código novo entrar sem testes também demanda tempo e fará, sim, com que as entregas demorem mais, especialmente nas primeiras semanas. O preço é esse e a diretoria precisa decidir se irá pagá-lo ou não.

A equipe pode tentar “adiantar umas horas”, mas é difícil dizer exatamente como será a adaptação ao novo processo e, portanto, a comunicação entre todas as partes precisa ser fluida e transparente.

Mas e o código antigo?

Boa pergunta.

Outra boa é a seguinte: vale a pena escrever testes novos para funcionalidades antigas?. E a resposta é: depende.

Alguns dizem que todo código sem testes é “código legado”. Código legado é aquele código que a equipe não domina bem o suficiente para garantir que tudo continuará funcionando caso a situação no entorno mude completamente.

Migrar da GCP para AWS? Hummm… absolutamente não temos como garantir que nada vai quebrar…

Então fica a critério da empresa. Num cenário ideal, alguém sempre deveria estar escrevendo testes para funcionalidades antigas, sejam unitários, de integração ou o que quer que seja.

Minha recomendação é que, a não ser que haja previsão de se reescrever a coisa toda do zero usando tecnologias completamente diferentes, testes devem ser escritos para funcionalidades antigas e quanto maior a cobertura (e qualidade), melhor para todos.

Curtiu? Então assine a minha newsletter e receba conteúdo interessante em pequenos drops direto na sua caixa de e-mails:

--

--