ti-enxame.com

A cobertura de teste é uma medida adequada da qualidade do código?

Se eu tiver algum código que tem 80% de cobertura de teste (todos os testes passam), é justo dizer que é de qualidade superior ao código sem cobertura de teste?

Ou é justo dizer que é mais sustentável?

21
David_001

Em um sentido estrito, não é justo fazer qualquer reclamação até que a qualidade do conjunto de testes seja estabelecida. Passar em 100% dos testes não é significativo se a maioria dos testes são triviais ou repetitivos entre si.

A questão é: na história do projeto, algum desses testes descobriu bugs? O objetivo de um teste é encontrar bugs. E se não o fizessem, eles falharam nos testes. Em vez de melhorar a qualidade do código, eles podem estar apenas dando a você uma falsa sensação de segurança.

Para melhorar seus designs de teste, você pode usar (1) técnicas de caixa branca, (2) técnicas de caixa preta e (3) teste de mutação.

(1) Aqui estão algumas boas técnicas de caixa branca para aplicar aos designs de teste. Um teste de caixa branca é construído com o código-fonte específico em mente. Um aspecto importante do teste de caixa branca é a cobertura do código:

  • Cada função é chamada? [Cobertura funcional]
  • Cada instrução é executada? [Cobertura de declaração - tanto a cobertura funcional quanto a cobertura de declaração são muito básicas, mas melhores do que nada]
  • Para cada decisão (como if ou while), você tem um teste que força ela a ser verdadeira e outro que força a ser falsa? [Cobertura de decisão]
  • Para cada condição que é uma conjunção (usa &&) ou disjunção (usa ||), cada subexpressão tem um teste onde é verdadeiro/falso? [Cobertura de condição]
  • Cobertura de loop: você tem um teste que força 0 iterações, 1 iteração, 2 iterações?
  • Cada break de um loop é coberto?

(2) As técnicas de caixa preta são usadas quando os requisitos estão disponíveis, mas o código em si não. Isso pode levar a testes de alta qualidade:

  • Seus testes de caixa preta cobrem vários objetivos de teste? Você vai querer que seus testes sejam "gordos": eles não apenas testam o recurso X, mas também testam Y e Z. A interação de diferentes recursos é uma ótima maneira de encontrar bugs.
  • O único caso em que você não deseja testes "gordos" é quando está testando uma condição de erro. Por exemplo, teste de entrada de usuário inválida. Se você tentou atingir vários objetivos de teste de entrada inválida (por exemplo, um CEP inválido e um endereço inválido), é provável que um caso esteja mascarando o outro.
  • Considere os tipos de entrada e forme uma "classe de equivalência" para os tipos de entrada. Por exemplo, se o seu código testa para ver se um triângulo é equilátero, o teste que usa um triângulo com lados (1, 1, 1) provavelmente encontrará os mesmos tipos de erros que os dados de teste (2, 2, 2) e (3, 3, 3) encontrará. É melhor gastar seu tempo pensando em outras classes de entrada. Por exemplo, se o seu programa lida com impostos, você vai querer um teste para cada faixa de imposto. [Isso é chamado de particionamento de equivalência.]
  • Casos especiais costumam estar associados a defeitos. Seus dados de teste também devem ter valores de limite, como aqueles em, acima ou abaixo das bordas de uma tarefa de equivalência. Por exemplo, ao testar um algoritmo de classificação, você desejará testar com uma matriz vazia, uma matriz de elemento único, uma matriz com dois elementos e, em seguida, uma matriz muito grande. Você deve considerar casos limite não apenas para entrada, mas também para saída. [Esta é a chamada análise de valor limite.]
  • Outra técnica é a "Adivinhação de erros". Você tem a sensação de que, se tentar alguma combinação especial, seu programa pode quebrar? Então experimente! Lembre-se: Seu objetivo é encontrar bugs, não confirmar que o programa é válido. Algumas pessoas têm o dom de adivinhar o erro.

(3) Por fim, suponha que você já tenha muitos bons testes para cobertura de caixa branca e técnicas de caixa preta aplicadas. O que mais você pode fazer? É hora de Teste seus testes. Uma técnica que você pode usar é o teste de mutação.

No teste de mutação, você faz uma modificação em (uma cópia de) seu programa, na esperança de criar um bug. Uma mutação pode ser:

Altere uma referência de uma variável para outra variável; Insira a função abs (); Alterar menor que para maior que; Exclua uma declaração; Substitua uma variável por uma constante; Exclua um método de substituição; Exclua uma referência a um super método; Alterar a ordem do argumento

Crie várias dezenas de mutantes, em vários lugares em seu programa [o programa ainda precisará ser compilado para ser testado]. Se seus testes não encontrarem esses bugs, agora você precisa escrever um teste que possa encontrar o bug na versão modificada do seu programa. Assim que um teste encontrar o bug, você matou o mutante e pode tentar outro.


Adendo: Esqueci de mencionar este efeito: Bugs tendem a se aglomerar. O que isso significa é que quanto mais bugs você encontrar em um módulo, maior será a probabilidade de encontrar mais bugs. Portanto, se você tiver um teste que falhou (ou seja, o teste foi bem-sucedido, já que o objetivo é encontrar bugs), não só deve corrigir o bug, mas também escrever mais testes para o módulo, usando o técnicas acima.

Enquanto você estiver encontrando bugs em uma taxa constante, os esforços de teste devem continuar. Somente quando houver um declínio na taxa de novos bugs encontrados, você deve ter certeza de que fez bons esforços de teste para essa fase de desenvolvimento.

25
Macneil

O código com absolutamente nenhum teste pode ser de qualidade extremamente alta, legível, bonito e eficiente (ou lixo total), então não, não é justo dizer que o código com 80% de cobertura de teste é de qualidade superior ao código sem cobertura de teste.

Poderia ser justo dizer que o código 80% coberto com bom testes é provavelmente de qualidade aceitável e provavelmente relativamente sustentável. Mas garante pouco, realmente.

7
Joonas Pulakka

Por uma definição, é mais fácil de manter, já que qualquer alteração significativa tem maior probabilidade de ser detectada pelos testes.

No entanto, o fato de o código passar nos testes de unidade não significa que ele seja intrinsecamente de qualidade superior. O código ainda pode estar mal formatado com comentários irrelevantes e estruturas de dados inadequadas, mas ainda pode passar nos testes.

Eu sei qual código prefiro manter e estender.

7
ChrisF

Eu diria que é mais refatorável. A refatoração fica extremamente fácil se o código for coberto por muitos testes.

Seria justo chamá-lo de mais sustentável.

3
Josip Medved

Eu concordaria sobre a parte sustentável. Michael Feathers postou recentemente um vídeo de uma excelente palestra chamada " A profunda sinergia entre testabilidade e bom design " na qual ele discute esse tópico. Na palestra ele diz que o relacionamento é unilateral, ou seja, um código bem projetado pode ser testado, mas o código testável não é necessariamente bem projetado.

É importante notar que o streaming de vídeo não é muito bom no vídeo, então pode valer a pena fazer o download se você quiser assistir na íntegra.

2
Paddyslacker