Devemos testar todos os nossos métodos?

54

Hoje eu conversei com meu colega de equipe sobre o teste de unidade. A coisa toda começou quando ele me perguntou "Ei, onde estão os testes para essa classe, eu vejo apenas um?". A turma inteira era um gerente (ou um serviço, se você preferir chamá-lo assim) e quase todos os métodos eram simplesmente delegar coisas para um DAO, de modo que era semelhante a:

SomeClass getSomething(parameters) {
    return myDao.findSomethingBySomething(parameters);
}

Uma espécie de clichê sem lógica (ou pelo menos eu não considero essa delegação simples como lógica), mas na maior parte dos casos, um clichê útil (separação de camadas, etc.). E tivemos uma longa discussão se devo ou não testar a unidade (acho que vale a pena mencionar que testei totalmente o DAO). Seus principais argumentos são que não era TDD (obviamente) e que alguém poderia querer ver o teste para checar o que este método faz (eu não sei como poderia ser mais óbvio) ou que no futuro alguém poderia querer mudar o método. implementação e adicionar nova lógica (ou mais como "qualquer") a ela (nesse caso, acho que alguém deveria simplesmente testar essa lógica ).

Isso me fez pensar, no entanto. Devemos nos esforçar para obter a maior cobertura de teste%? Ou é simplesmente uma arte pela arte, então? Eu simplesmente não vejo nenhum motivo por trás de testar coisas como:

  • getters e setters (a menos que tenham alguma lógica neles)
  • código "boilerplate"

Obviamente, um teste para esse método (com zombarias) levaria menos de um minuto, mas acho que ainda é tempo desperdiçado e um milissegundo maior para cada IC.

Existe alguma razão racional / não "inflamável" para por que se deve testar cada linha de código (ou tantas quantas ele puder)?

    
por Zenzen 19.01.2012 / 21:40
fonte

10 respostas

44

Eu uso a regra de Kent Beck:

Teste tudo o que puder acontecer.

Claro, isso é subjetivo até certo ponto. Para mim, getters / setters triviais e one-liners como o seu acima geralmente não valem a pena. Mas, novamente, passo a maior parte do tempo escrevendo testes unitários para código legado, apenas sonhando com um projeto TDD greenfield ... Em tais projetos, as regras são diferentes. Com o código legado, o objetivo principal é cobrir o máximo de terreno com o menor esforço possível, de modo que os testes de unidade tendem a ser de nível mais alto e mais complexos, mais como testes de integração se for pedante em termos de terminologia. E quando você está lutando para obter uma cobertura geral de código acima de 0%, ou apenas conseguiu aumentar em mais de 25%, os getters e setters de teste de unidade são a menor das suas preocupações.

OTOH em um projeto TDD greenfield, pode ser mais prático escrever testes mesmo para tais métodos. Especialmente como você já escreveu o teste antes de ter a chance de começar a se perguntar "esta linha vale um teste dedicado?". E pelo menos esses testes são fáceis de escrever e rápidos de executar, então não é um grande problema.

    
por 19.01.2012 / 21:49
fonte
12

Existem alguns tipos de testes unitários:

  • Baseado em estado. Você age e, em seguida, afirma contra o estado do objeto. Por exemplo. Eu faço um depósito. Eu então verifico se o saldo aumentou.
  • Valor de retorno baseado. Você age e se posiciona contra o valor de retorno.
  • Interação baseada. Você verifica se seu objeto chamou outro objeto. Isso parece ser o que você está fazendo no seu exemplo.

Se você fosse escrever seu teste primeiro, então faria mais sentido - como você esperaria chamar uma camada de acesso a dados. O teste falharia inicialmente. Você então escreveria o código de produção para fazer o teste passar.

Idealmente, você deve testar o código lógico, mas as interações (objetos chamando outros objetos) são igualmente importantes. No seu caso, eu faria

  • Verifique se chamei a camada de acesso a dados com o parâmetro exato que foi passado.
  • Verifique se foi chamado apenas uma vez.
  • Verifique se retorno exatamente o que me foi dado pela camada de acesso a dados. Caso contrário, eu também poderia retornar null.

Atualmente não há lógica, mas nem sempre será o caso.

No entanto, se você tiver certeza de que não haverá lógica nesse método e provavelmente permanecerá o mesmo, eu consideraria chamar a camada de acesso a dados diretamente do consumidor. Eu faria isso apenas se o resto da equipe estivesse na mesma página. Você não quer enviar uma mensagem errada para a equipe, dizendo "Ei pessoal, é bom ignorar a camada de domínio, basta chamar a camada de acesso a dados diretamente".

Eu também me concentraria em testar outros componentes se houvesse um teste de integração para esse método. Ainda estou vendo uma empresa com testes de integração sólidos.

Tendo dito tudo isso - eu não testaria cegamente tudo. Eu estabeleceria os hot spots (componentes com alta complexidade e alto risco de quebra). Eu então me concentraria nesses componentes. Não faz sentido ter uma base de código onde 90% da base de código é bastante direta e é coberta por testes de unidade, quando os 10% restantes representam a lógica central do sistema e não são cobertos por testes de unidade devido à sua complexidade.

Por fim, qual é o benefício de testar esse método? Quais são as implicações se isso não funcionar? Eles são catastróficos? Não se esforce para obter uma cobertura de código alta. A cobertura de código deve ser um subproduto de um bom conjunto de testes de unidade. Por exemplo, você pode escrever um teste que andará na árvore e lhe dará 100% de cobertura desse método, ou você pode escrever três testes de unidade que também lhe darão 100% de cobertura. A diferença é que, ao escrever três testes, você testa os casos de borda, em vez de apenas caminhar pela árvore.

    
por 28.03.2014 / 00:43
fonte
8

Aqui está uma boa maneira de pensar sobre a qualidade do seu software:

    A verificação de tipo
  1. está lidando com parte do problema.
  2. O teste de
  3. lidará com o restante

Para funções padrão e trivial, você pode confiar na verificação de tipos fazendo seu trabalho e, para o resto, você precisa de casos de teste.

    
por 19.01.2012 / 22:19
fonte
6

Na minha opinião, a complexidade ciclomática é um parâmetro. Se um método não for complexo o suficiente (como getters e setters). Nenhum teste unitário é necessário. O nível de Complexidade Cyclomática de McCabe deve ser maior que 1. Outra palavra deve ter no mínimo 1 declaração de bloco.

    
por 20.10.2012 / 22:03
fonte
1

Quando se deparar com uma questão filosófica, recue para os requisitos de direção.

O seu objetivo é produzir software razoavelmente confiável a um custo competitivo?

Ou é para produzir software da maior confiabilidade possível, quase que independentemente do custo?

Até certo ponto, os dois objetivos de qualidade e velocidade / custo de desenvolvimento se alinham: você gasta menos tempo escrevendo testes do que corrigindo defeitos.

Mas além desse ponto, eles não. Não é tão difícil conseguir, digamos, um bug reportado por desenvolvedor por mês. Reduzir para um por dois meses apenas libera um orçamento de talvez um dia ou dois, e esse teste extra provavelmente não reduzirá pela metade sua taxa de defeitos. Portanto, não é mais uma simples vitória / vitória; você tem que justificá-lo com base no custo do defeito para o cliente.

Esse custo varia (e, se você quiser ser mau, sua capacidade de impor esses custos a você, seja através do mercado ou de uma ação judicial). Você não quer ser mal, então você conta esses custos de volta na íntegra; às vezes, alguns testes ainda tornam o mundo mais pobre por sua existência.

Em suma, se você tentar cegamente aplicar os mesmos padrões a um site interno como o software de voo de avião de passageiros, você acabará fora do negócio ou na cadeia.

    
por 28.03.2014 / 00:11
fonte
1

Um ressonante SIM com TDD (e com algumas exceções)

Controversa, tudo bem, mas eu diria que qualquer um que responda 'não' a essa pergunta está perdendo um conceito fundamental de TDD.

Para mim, a resposta é um retumbante sim se você seguir o TDD. Se você não for, então não, é uma resposta plausível.

O DDD em TDD

O TDD é frequentemente citado como tendo os principais benefícios.

  • Defesa
    • Garantir que o código pode mudar , mas não é o seu comportamento .
    • Isso permite a prática sempre importante da refatoração .
    • Você ganha este TDD ou não.
  • Design
    • Você especifica o que algo deve fazer, como deve se comportar antes de implementá-lo
    • .
    • Isso geralmente significa decisões de implementação mais fundamentadas .
  • Documentação
    • O conjunto de testes deve servir como documentação especificação (requisitos).
    • O uso de testes para esse fim significa que a documentação e a implementação estão sempre em estado consistente - uma alteração para um significa uma alteração para outro. Compare com a manutenção de requisitos e design em documentos separados do Word.

Separe a responsabilidade da implementação

Como programadores, é terrivelmente tentador pensar em atributos como algo significativo e como um tipo de sobrecarga.

Mas atributos são um detalhe de implementação, enquanto setters e getters são a interface contratual que realmente faz os programas funcionarem.

É muito mais importante soletrar que um objeto deveria:

Allow its clients to change its state

e

Allow its clients to query its state

em seguida, como esse estado é realmente armazenado (para o qual um atributo é o mais comum, mas não o único).

Um teste como

(The Painter class) should store the provided colour

é importante para a parte documentação do TDD.

O fato de que a eventual implementação é trivial (atributo) e não traz nenhum benefício defense deve ser desconhecido para você quando você escreve o teste.

A falta de engenharia de ida e volta ...

Um dos principais problemas no mundo do desenvolvimento de sistemas é a falta de engenharia de ida e volta 1 - o processo de desenvolvimento de um sistema é fragmentado em subprocessos desconexos os artefatos dos quais (documentação, código) são frequentemente inconsistentes.

1 Brodie, Michael L. "John Mylopoulos: costura sementes de modelagem conceitual." Modelagem Conceitual: Fundamentos e Aplicações. Springer Berlin Heidelberg, 2009. 1-9.

... e como o TDD resolve isso

É a parte documentação do TDD que garante que as especificações do sistema e seu código sejam sempre consistentes.

Projete primeiro, implemente depois

No TDD, escrevemos primeiro o teste de aceitação falho, depois escrevemos o código que os permite passar.

Dentro do BDD de nível superior, escrevemos os cenários primeiro, depois os fazemos passar.

Por que você deve excluir setters e getter?

Em teoria, é perfeitamente possível dentro de TDD para uma pessoa escrever o teste, e outra para implementar o código que a faz passar.

Então pergunte a si mesmo:

Should the person writing the tests for a class mention getters and setter.

Como os getters e setters são uma interface pública para uma classe, a resposta é obviamente yes , ou não haverá maneira de definir ou consultar o estado de um objeto.

Obviamente, se você escrever o código primeiro, a resposta pode não ser tão clara.

Exceções

Existem algumas exceções óbvias a esta regra - funções que são detalhes de implementação de clearcut e claramente não fazem parte do design do sistema.

Por exemplo, o método local 'B ()':

function A() {

    // B() will be called here    

    function B() {
        ...
    }
} 

Ou a função privada square() aqui:

class Something {
private:
    square() {...}
public:
    addAndSquare() {...}
    substractAndSquare() {...}
}

Ou qualquer outra função que não faça parte de uma interface public que precise de ortografia no design do componente do sistema.

    
por 18.11.2015 / 15:18
fonte
0

Sua resposta sobre isso depende da sua filosofia (acredite que é Chicago x Londres? Tenho certeza de que alguém vai procurá-la). O júri ainda está longe disso na abordagem mais eficiente em termos de tempo (porque, afinal, esse é o maior impulsionador desse tempo - menos tempo gasto em correções).

Algumas abordagens dizem testar apenas a interface pública, outras dizem testar a ordem de cada chamada de função em cada função. Muitas guerras sagradas foram travadas. Meu conselho é tentar as duas abordagens. Escolha uma unidade de código e faça como X, e outra como Y. Depois de alguns meses de teste e integração, volte e veja qual deles melhor atende às suas necessidades.

    
por 21.01.2012 / 02:26
fonte
0

É uma pergunta complicada.

A rigor, eu diria que não é necessário. Você está melhor escrevendo testes de unidade de estilo BDD e de nível de sistema que asseguram que os requisitos de negócios funcionem como pretendido em cenários positivos e negativos.

Dito isto, se o seu método não estiver coberto por esses casos de teste, você terá que questionar por que ele existe e se é necessário, ou se há requisitos ocultos no código que não estão refletidos em sua documentação ou histórias de usuário que devem ser codificadas em um caso de teste de estilo do BDD.

Pessoalmente, gosto de manter cobertura por linha em torno de 85-95% e gate check-ins para mainline para garantir que a cobertura de teste de unidade existente por linha atinja esse nível para todos os arquivos de código e que nenhum arquivo seja descoberto.

Supondo que as melhores práticas de teste estão sendo seguidas, isso dá muita cobertura, sem forçar os desenvolvedores a perder tempo tentando descobrir como obter cobertura adicional em código difícil de exercitar ou código trivial simplesmente por causa da cobertura.

    
por 20.01.2012 / 20:50
fonte
-1

O problema é a questão em si, você não precisa testar todos os "methdos" ou todas as "classes" necessárias para testar todos os recursos de seus sistemas.

Seu principal pensamento em termos de recursos / comportamentos, em vez de pensar em termos de métodos e classes. Claro que um método está aqui para fornecer suporte para um ou mais recursos, no final todo o seu código é testado, pelo menos, todo o código é importante na sua base de código.

Em seu cenário, provavelmente essa classe "manager" é redundante ou desnecessária (como todas as classes com um nome que contenha a palavra "manager"), ou talvez não, mas parece um detalhe de implementação, provavelmente essa classe não merece um teste de unidade porque essa classe não possui nenhuma lógica de negócios relevante. Provavelmente você precisa desta classe para algum recurso funcionar, o teste para esse recurso cobre essa classe, dessa forma você pode refatorar essa classe e ter um teste que verifica se a coisa que importa, seus recursos, ainda funciona após o refatorador.

Pense em recursos / comportamentos que não estão nas classes de método, não posso repetir isso o suficiente.

    
por 28.03.2014 / 03:18
fonte
-4

This made me think, though. Should we strive for the highest test coverage %?

Sim, idealmente 100%, mas algumas coisas não são testáveis por unidade.

getters and setters (unless they actually have some logic in them)

Getters / Setters são estúpidos - apenas não os use. Em vez disso, coloque sua variável de membro na seção pública.

"boilerplate" code

Obtenha o código comum e teste a unidade. Isso deve ser tão simples assim.

Are there any rational/not "flammable" reasons to why one should test every single (or as many as he can) line of code?

Por não fazer isso, você pode perder alguns erros muito óbvios. Os testes de unidade são como uma rede segura para capturar certos tipos de bugs, e você deve usá-los o máximo possível.

E a última coisa: estou em um projeto onde as pessoas não queriam perder tempo escrevendo testes de unidade para algum "código simples", mas depois decidiram não escrever nada. No final, partes do código se transformaram em grande bola de lama .

    
por 19.01.2012 / 22:22
fonte