Executando verificação de segurança...
5

O Mito do Teste Unitário: 100% de Cobertura de Código e um Software Bugado

Já reparou como alguns desenvolvedores tratam testes unitários como se fosse o Santo Graal da qualidade de software? Eles perseguem com um fervor essa meta de alcançar 100% de cobertura de código atráves de testes unitários. Como se isso fosse garantir um software perfeito. No entanto, esse mito é tão real quanto unicórnios.

Vamos ilustrar com nosso velho amigo, o relógio simples de 24 horas:

class Relogio:
    def __init__(self):
        self.hora = 0

    def avancar_hora(self):
        self.hora += 1


def teste_inicializacao():
    relogio = Relogio()
    assert relogio.hora == 0

def teste_avancar_hora():
    relogio = Relogio()
    relogio.avancar_hora()
    assert relogio.hora == 1

Ótimo! Temos testes unitários que cobrem todo o código. Mas espere... depois das "23 horas", não deveríamos voltar para "0"? Pois é... os testes esqueceram disso. Parabéns pela cobertura total inútil!

Mas calma, a cobertura do código tem seu valor quando está alinhada aos testes do sistema - ela ajuda a identificar partes negligenciadas no código e melhorá-lo.

Imagine uma situação onde só temos testes end-to-end com requisitos extensos que sozinhos alcançam a sonhada cobertura total. Eu acreditaria minha vida neste software mesmo sem um único teste unitário.

Porém devemos admitir a importância de testes unitários na manutenção da qualidade do código e na facilitação das mudanças nos componentes sem impacto geral significativo.

No fim das contas, eles são importantes sim! Mas sem requisitos claros para orientá-los são literalmente como atirar em alvos imaginários.

E vocês colegas desenvolvedores? Continuarão perseguindo unicórnios ou preferem encarar a realidade dura e fria? Se você deseja um software livre de bugs, deixe de lado os testes unitários e abrace a especificação de requisitos.

Deixem suas opniões nos comentários!
Leia também este outro artigo escrito na sequência que expande sobre as ideias abordadas aqui.
V&V - Verificação, Fazendo do Jeito Correto && Validação, Fazendo a Coisa Certa

Carregando publicação patrocinada...
3

Muito bom o exemplo da cobertura.

Devemos sempre lembrar da pirâmide de testes (unitário, integração e e2e), sendo da esquerda pra direita o mais importante e inversamente o que gera mais ruído.

Já tive experiências de sistemas que utilizavam somente e2e, e a qualidade do sistema era horrível. Os testes falhavam o tempo todo devido a fatores externos, dependiam do estado anterior de outros testes e sempre geravam enorme ruído. As vezes um grande número deles falhava e como um pequeno bug afetava várias partes do sistema, sem testes unitários/integração era praticamente ficar caçando uma agulha num palheiro por falta de visibilidade. Ou seja, os e2e sozinhos não ajudavam muito.

2

Já trabalhei com um sistema que inicialmente tinha testes e2e que rodavam diariamente, mas chegou uma hora que para rodar tudo demorava mais de 13 horas, qur dizer, nada prático ou viável.
Nisso um colega trouxe esse conhecimento da pirâmide de testes, separamos o que deveria ser UT, o que poderia ser de integração e diminuímos muito os e2e. O resultado disso foi a diminuição para cerca de 5-6 horas para ter uma cobertura e segurança bem melhores.

Mas como dito no post, se não sabemos o que deve ser efetivamente testado, tudo isso é em vão...

2

Até entendo sua critica de no começo nos acabarmos caindo em armadilhas de se cegar por metricas imprecisas ou até ruins. Isso não só ocorre na hora do desenvolvimento mas também na gestão. Quem nunca viu um PO/PM/Agilista/Scrum Master falar um monte de metricas vazias como story points, troughtput ou pasmem linhas de codigo escritas...

A cobertura teorica de 100% é sim util, sendo a "cobertura teorica" testes unitarios que garantem que todos os use cases possiveis de um objeto, função, script estejam definidos em um teste. Isso seria um ideial que teoricamente nos deixaria livres de bugs deterministicos (ficariamos sujeitos a bugs imprevisiveis como por exemplo bitflip por radiação).

Agora, no mundo real isso não é possivel. Para entender cobertura precisamos entender como a cobertura é calculada pelos frameworks. Geralmente os frameworks calculam isso baseado nas linhas de codigo que vão ser executadas nos testes, claramente isso não é o suficiente para atestar a qualidade de um software, que por mais que alguns frameworks consigam ainda diferenciar desvios condicionais isso não garante que todos os use cases foram testados. De fato como como apontou os testes não só tem uma caracterisca quantitativa mas também uma qualitativa.

Ainda sim uma cobertura de 100% calculada por esses frameworks muito provavelmente em casos concretos é o suficiente para termos uma boa noção do controle de qualidade daquele software. Um software com 80% de cobertura e um com 100% de cobertura provavelmente tem um controle de qualidade melhor que um de 80%.

Mesmo no seu exemplo tu disses que a cobertura é inutil. E isso não é verdade. Existe sim uma utilidade ai. O primeiro teste está ótimo, ele assegura que o objeto instanciado está iniciando como deveria. O segundo teste também é bom, de certa forma até redundante, pois ele atesta que o relogio inicia com 0 e ao executar avancar_hora() ele incremente +1 no marcador. Isso definitivamente não é "inutil". Podemos dizer que seus testes são incompletos ou de qualidade baixa, mas inutil nunca.

Ta ai uma coisa que temos que entender sobre testes e acredito que seja essa sua intenção a o escrever esse post: Escrever bons testes faz parte da sua skill como programador. Assim como tu precisas aprender desing de código, arquitetura de software, ciclo de vida, aindas deve aprender como escrever bons testes. Testar é muito mais difícil do que a maioria pensa, mas com pratica deliberada podemos melhorar isso como qualquer outro skill da sua carreira.

No fim das contas, eles são importantes sim! Mas sem requisitos claros para orientá-los são literalmente como atirar em alvos imaginários.

Também entendo o que quis dizer aqui mas acho que precisamos explicar melhor. Existem bugs e existem regras de negocio mal especificadas. Eu sei que no coditiano nos costumamos misturar os dois mas existem diferenças:

Um bug é um problema de software, um comportamento não esperado na hora de executar um codigo. Por exemplo um numero que estoura o limite de bits, uma string que entra na função mas era para entrar como inteiro, uma logica mal escrita que não preve certos inputs de dados etc...

Um bug é diferente de um requisito mal especificado. Vamos a o exemplo do relogio, a tua area de produto te manda algo assim:

"Crie um relogio. O relogio deve ter uma funcionalidade que mostra a hora e uma funcionalidade que a o acionar incrementa o mostrador com mais 1 hora."

No caso, o código acima estaria 100% dentro da especificação.

"Ah! Mas sabemos o que é um relogio, o relogio conta as horas e quando chega no 23 o proximo é 0"

Sabemos mesmo??? E se for um outro tipo de relogio, aqui por exemplo chamamos o medidor de gasto de energia eletrica e volume de água de relogio. E se for um relogio no formato americano que a partir do 12 vai para 1? E se o relogio é para contar as horas a partir de um certo momento, identido a o unix time que mostra a data e hora em segundos desde 1970? E se o relogio é lunar.... (kkkk)

Por isso que como realmente precisamos de requisitos claros do que é necessario para escrever os testes e não só os testes como o software produtivo em si. E é por isso que também é seu trabalho como programador é fazer perguntas antes e durante o desenvolvimento!

E vocês colegas desenvolvedores? Continuarão perseguindo unicórnios ou preferem encarar a realidade dura e fria? Se você deseja um software livre de bugs, deixe de lado os testes unitários e abrace a especificação de requisitos.
Deixem suas opniões nos comentários!

Aqui eu acho meio problematico... Não, não deixe os testes unitarios! Os testes unitarios é a sua primeira linha de frente contra os bugs. Os testes unitarios tem certos beneficios que os outros não tem (intergração ou end to end).

  • Os testes unitarios são otimos para testar as regras de negocio (use cases), pois eles sofrem menos ruido
  • Os testes unitarios são rapidos de executar
  • Os testes unitarios deixam claro o objetivo de um pequeno trecho de codigo, serve como documentação.

Testes unitarios é uma bala de prata? Não, obvio que não! Um software sem testes unitarios necessarimente é de baixa qualidade? Não, também não. Mas isso não significa que é uma boa pratica largar eles para fazer outros tipos de testes.

Todos os tipos de testes devem ser adotados na sua aplicação dependendo da complexidade e sensibilidade. Todos os testes cumpre partes que outros testes são dificeis de fazer. Testes e2e são otimos para atestar que todas as tomadas (ligaçoes externas) do software estão bem conectadas, coisas que os testes unitarios não fazem, mas seria muito lento e complicado testar as regras de negocio com ele entre outras coisas.

No fim, em linhas gerais, concordo com o que foi dito pelo colega clacerda, só acho que a explicação precisava ser um pouco mais profunda. Upvote no post! :D

1
1

Excelente, muito bom mesmo. O seu outro post complementa muito o que tem no post root aqui. Talvez os dois textos deveriam estar juntos, pelo menos o link.

2

O percentual de coverage é importante sim, mas ele não significa que os testes são bem escritos.

Isso acontece por diversos motivos, um deles é que os testes são escritos ou com base no próprio código (quando criados posteriormente), ou com base em especificações de projeto.

E essas especificações podem não estar corretas, ou incompletas, como o caso que você citou.

Isso torna um codigo 100% testado inútil? Não! Continua sendo importante uma alta taxa de cobertura.

O que torna esse código seguro são as diversas etapas do desenvolvimento, diversas firmas diferentes de testar (unit, integration, e2e), além do bom e velho humano fazendo user testing.

Na hora que esse erro for detectado, alguem escreverá um teste que o detecte.

Ah, mas eu preciso esperar um erro acontecer pra poder testar? As vezes sim. Há situações que ninguém nunca pensou, e em produção se descobre. O importante dos testes automatizados é que você dificilmente irá repetir aquele bug, pois existem testes escritos para detectar ele.

Enfim, testes não deixam de ser excelentes ferramentas, mas precisam ser escritos constantemente e atualizados.

1
1

Quando iniciamos na área da programação, acabamos caindo nessas armadilhas. Eu já caí nela. Achava que tinha que existir 100% de cobertura, e testava só para ver o gráfico indicando que todo o código foi "passado" pelo teste.

Com o tempo e amadurecimento, percebemos que o ideal é entender os casos de uso, e testar esses casos. Por isso faz sentido o que você falou sobre especificação de requisitos.

Agora, não devemos deixar de lado a parte unitária da coisa, pois elas garantem sim qualidade e tendem a executar mais rápido que os testes E2E.

Tenho percebido que o ideal é uma mistura de DDD com testes de vários níveis, conforme pirâmide de testes que já foi citada.