sexta-feira, 27 de novembro de 2009

Pare de escrever testes inúteis

É bom testar seu código. Ninguém discute isso. Montar um conjunto de testes automatizados pode ajudar em muito a qualidade do seu sistema. O problema é que muitas vezes medimos a qualidade da nossa base de testes unitários pelo percentual de linhas de código que é exercitada pelos testes. Chegar a 100% de cobertura é o alvo sagrado quase inatingível. Infelizmente, essa batalha é a batalha errada por dois motivos.

Primeiro e menos grave, ter uma cobertura de testes alta dá uma falsa sensação de segurança. Se temos 80% de cobertura, então 80% do código não tem erros, certo? Não exatamente. O que estamos medindo é cobertura de comandos, que é a forma mais fraca de cobertura. Eis um exemplo simples:

if ((a > 0) || (b < 0)) {
    c = 0;
} else {
    c = 1;
}

Bastam dois testes, um com a==0 e outro com a==1 para cobrirmos todas as linhas. No entanto, se houver um erro na segunda cláusula (se tivesse de ser b > 0, por exemplo), nossos testes não detectarão o erro. Um testador experiente sabe que tem de testar as duas cláusulas, mas um inexperiente pode ser ludibriado pela medida de 100% de cobertura desse código e pensar que não há nada mais a fazer.

Segundo e mais grave, há testes que simplesmente não fazem sentido. Já vi inúmeros testes em que o testador está simplesmente repetindo todo o comportamento da classe sendo testada. Quando se usa frameworks como EasyMock ou Mockito, fica ainda mais fácil cometer esse erro. Esses frameworks nos permitem verificar se determinados métodos estão sendo chamados com certos parâmetros. Se só o que o método faz é chamar os métodos x, y e z, o que acaba acontecendo é que o teste unitário simplesmente verifica se x, y e z são chamados nessa ordem. Qual é o erro que pode ser encontrado por esse teste unitário? Nenhum. Só o que acontece é que quem modificar o método vai ter de modificar seu clone do mal nos testes unitários.

Imagine uma classe de modelo cujo objetivo é receber estruturas de dados internas e convertê-las em chamadas ao banco de dados. Como testar essa classe? Fazendo um mock do banco, chamando a classe com alguns valores concretos, e observando que o banco está sendo chamado com os valores corretos. Qual o valor de se fazer esse teste? Quase nenhum. O único valor é que você está efetivamente duplicando a funcionalidade que você fez na classe original e, se as duas implementações forem diferentes, você vai ter de olhar pra ver qual das duas está errada. É uma versão do fenômeno da segunda vez.

Quando faz sentido fazer testes de unidade, então? Pra mim, só quando a classe tem lógica interna, ou seja, toma decisões ou faz alguma computação real que pode ser observada externamente. O ideal é poder observar as mudanças no estado no sistema, e não o seu fluxo de controle.  Traduzindo:

int metodo(int a) {
  int b = w(a);
  z(b);
  if (b > 0) {
    return x(b);
  } else {
    return y(b);
  }
}

Como deve ser um teste para esse método? Deve ser um teste caixa-preta. O teste deve testar os valores retornados são compatíveis com diferentes valores de entrada de a. Nada mais. Para isso precisamos entender o que w(a)x(b) e y(b) retornam. E z(b)?  Como sempre é executado, não faz sentido testá-lo. E devemos verificar se x(b) ou y(b) foram chamados? Na minha opinião, não. Devemos é saber se os valores de retorno esperados são equivalentes aos recebidos.

O bom teste deve desafiar o código sendo testado. Deve criar várias situações distintas e observar se o código reage de acordo com o desejado. Quanto mais forte o desafio, mais útil o teste. Desafios fracos criam testes fracos.

E daí, onde quero chegar com tudo isso? daí que criar e manter testes tem um custo, e esse custo não é trivial. Vamos então à lei de Torsten sobre testes unitários:
Todo teste unitário deve trazer valor a longo prazo maior que seu custo de criação e manutenção.
Aqui, o valor do teste é proporcional ao desafio que ele faz ao código testado.

Resumindo, é OK não chegar nem perto de 100% de cobertura. Teste apenas o que faz sentido, lembre-se que nem todo teste é útil. E quando fizer sentido, teste direito, não se esqueça de testar nenhum caso importante só porque já cobriu todos os comandos.

4 comentários:

  1. Olá, sou um leitor assíduo do seu blog, e acho bastante interessante. Você está de parabéns. Mas sobre esse seu atual post tenho um comentário, não fazer testes para classes que não tenha "lógica" acho meio perigoso. Sempre pode ter um estagiário (sempre ele, coitado) que pode vir, mexer na classe, inserir uma "lógica" e acabar com tudo... =)

    ResponderExcluir
  2. Mas aí como é que você faz, esconde os testes do estagiário? Não é só ele ir lá no teste e inserir a mesma "lógica" lá?

    ResponderExcluir
  3. O erro aqui é testar a implementação ao invés de testar a interface. Em geral, esse tipo de vício acontece quando o teste unitário é escrito a posteriori. Se o desenvolvedor utiliza TDD, e o teste é escrito antes do código, a preocupação deixa de ser com os detalhes da implementação, que vão mudar com o tempo anyway; e passa a ser com o que interessa de verdade, que são os invariantes da api. O Misko tem um artigo bom sobre o tema:

    http://misko.hevery.com/2009/09/02/it-is-not-about-writing-tests-its-about-writing-stories/

    Uma sinal simples pra detectar esse tipo de problema é verificar quantos dos seus testes usam nice mocks ao invés de stricts. Se você tem poucos nice, é sinal de que seus testes devem estar amarrados demais na implementação.

    ResponderExcluir
  4. Ricbit, concordo plenamente que esse problema só acontece quando não se faz TDD. Só que tem pouca gente (ninguém?) em minha volta fazendo TDD... aí eu enxergo essas aberrações de teste com frequência.

    ResponderExcluir