Devemos testar todos os nossos métodos?

62

Hoje, conversei com meu colega sobre testes de unidade. A coisa toda começou quando ele me perguntou "ei, onde estão os testes para essa aula, eu vejo apenas um?". A classe inteira era um gerente (ou um serviço, se você preferir chamar assim) e quase todos os métodos estavam simplesmente delegando coisas a um DAO, por isso era semelhante a:

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

Um tipo de clichê sem lógica (ou pelo menos não considero uma delegação tão simples como lógica), mas um clichê útil na maioria dos casos (separação de camadas etc.). E tivemos uma discussão bastante demorada sobre se eu deveria testá-lo ou não na unidade (acho que vale a pena mencionar que fiz totalmente o DAO). Seus principais argumentos são que não era TDD (obviamente) e que alguém pode querer fazer o teste para verificar o que esse método faz (não sei como isso pode ser mais óbvio) ou que no futuro alguém possa querer alterar o implementação e adicione uma nova (ou mais parecida com "qualquer") lógica 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 em%? Ou então é simplesmente uma arte pelo bem da arte? Simplesmente não vejo razão para testar coisas como:

  • getters e setters (a menos que eles realmente tenham alguma lógica neles)
  • código "clichê"

Obviamente, um teste para esse método (com zombaria) levaria menos de um minuto, mas acho que ainda é tempo perdido e um milissegundo a mais para cada IC.

Existem razões racionais / não "inflamáveis" pelas quais alguém deve testar todas as linhas de código (ou tantas quanto puder)?

Zenzen
fonte
2
Ainda estou decidindo sobre essa questão, mas aqui está uma conversa de alguém que decidiu que a resposta é "não". Ian Cooper: TDD, onde tudo deu errado Para resumir essa grande conversa, você deve testar de fora para dentro e testar novos comportamentos, não novos métodos.
Daniel Kaplan
Esta é realmente uma ótima conversa, imperdível, uma palestra de abrir os olhos para muitas pessoas, eu adoro isso. Mas acho que a resposta não é "não". É "sim, mas indiretamente". Ian cooper fala sobre arquitetura hexagonal e recursos / comportamentos de teste que zombam / bloqueiam as portas. Nesse caso, essas portas são do DAO e esse "gerente / serviço" é testado não com um teste de unidade individual apenas para esta classe, mas com um "teste de unidade" (unidade na definição de Ian Cooper com a qual concordo completamente) que testa algum recurso no seu domínio que usa esse gerente / serviço.
AlfredoCasado 28/03
Depende do seu sistema até certo ponto, se você estiver desenvolvendo um sistema com um nível de certificação de segurança de moderado a alto, precisará cobrir todos os métodos, independentemente da trivialidade
jk.

Respostas:

49

Eu segui a regra de ouro de Kent Beck:

Teste tudo o que poderia quebrar.

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 de unidade para código legado, apenas sonhando com um bom projeto greenfield TDD ... Em tais projetos, as regras são diferentes. Com o código legado, o objetivo principal é cobrir o máximo de terreno com o mínimo de esforço possível, de modo que os testes de unidade tendem a ser de nível mais alto e mais complexo, mais parecidos com testes de integração, se alguém é pedante quanto à terminologia. E quando você está lutando para obter uma cobertura geral de código de 0%, ou apenas consegue aumentar 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 esses métodos. Especialmente porque você já escreveu o teste antes de ter a chance de começar a se perguntar "essa linha vale um teste dedicado?". E pelo menos esses testes são triviais para escrever e rápidos para executar, por isso não é grande coisa de qualquer maneira.

Péter Török
fonte
Ah, eu esqueci totalmente essa citação! Acho que vou usá-lo como meu principal argumento, porque, francamente - o que pode acontecer aqui? Não é muita coisa. A única coisa que pode quebrar é a invocação do método e, se isso acontecer, significa que algo realmente ruim aconteceu. Obrigado!
Zenzen 19/01/12
5
@Zenzen: "o que pode quebrar aqui? Na verdade, não muito." - Então pode quebrar. Apenas um pequeno erro de digitação. Ou alguém adiciona algum código. Ou atrapalha a dependência. Eu realmente acho que Beck alegaria que seu exemplo principal se qualifica como quebrável. Getters e setters, menos ainda, embora eu tenha me flagrado em um erro de copiar / colar, mesmo assim. A verdadeira questão é: se é muito trivial escrever um teste, por que ele existe?
Pd
11
A quantidade de tempo que você passou pensando nisso já poderia ter escrito o teste. Eu digo escrever o teste, não deixe quando não escrever um teste como uma área cinza, mais janelas quebradas aparecerão.
kett_chup
11
Acrescentarei que minha experiência geral é que testar getters e setters é algo valioso a longo prazo, mas de baixa prioridade. O motivo é que, por que ela tem uma chance "zero" de encontrar um bug agora, você não pode garantir que outro desenvolvedor não adicione algo em três meses ("apenas uma simples declaração") que terá a chance de quebrar . Ter um teste de unidade no lugar protege contra isso. Ao mesmo tempo, não é uma prioridade muito alta, porque você não encontrará nada tão cedo assim.
dclements
7
Testar cegamente tudo o que poderia quebrar não faz sentido. É preciso haver uma estratégia em que os componentes de alto risco sejam testados primeiro.
CodeART 27/03
13

Existem alguns tipos de teste de unidade:

  • Estado baseado. Você age e depois afirma contra o estado do objeto. Por exemplo, eu faço um depósito. Depois, verifico se o saldo aumentou.
  • Valor de retorno baseado. Você age e afirma contra o valor de retorno.
  • Baseado em interação. Você verifica se seu objeto chamou outro objeto. Parece ser o que você está fazendo no seu exemplo.

Se você escrevesse seu teste primeiro, isso faria mais sentido - como seria de esperar chamar uma camada de acesso a dados. O teste falharia inicialmente. Você 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 eu retorno exatamente o que me foi dado pela camada de acesso a dados. Caso contrário, eu também poderia retornar nulo.

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, consideraria chamar a camada de acesso a dados diretamente do consumidor. Eu faria isso apenas se o restante da equipe estivesse na mesma página. Você não deseja enviar uma mensagem errada para a equipe dizendo "Ei pessoal, tudo bem ignorar a camada de domínio, basta ligar diretamente para a camada de acesso a dados".

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

Dito tudo isso - eu não testaria cegamente tudo. Eu estabeleceria os pontos de acesso (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 em que 90% da base de código é bastante direta e é coberta por testes de unidade, quando 10% restantes representam a lógica principal do sistema e não são cobertos por testes de unidade devido à sua complexidade.

Finalmente, 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 alta cobertura de código. A cobertura do código deve ser um subproduto de um bom conjunto de testes de unidade. Por exemplo, você pode escrever um teste que irá percorrer a árvore e fornecer 100% de cobertura desse método, ou você pode escrever três testes de unidade que também fornecerão 100% de cobertura. A diferença é que, ao escrever três testes, você testa casos extremos, em vez de simplesmente andar na árvore.

CodeART
fonte
Por que você verificaria se o seu DAL foi chamado apenas uma vez?
Marjan Venema
9

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

  1. a verificação de tipo está lidando com parte do problema.
  2. teste vai lidar com o resto

Para clichês e funções triviais, você pode contar com a verificação de tipo para realizar seu trabalho e, para o resto, precisa de casos de teste.

tp1
fonte
É claro que a verificação de tipo só funciona se você estiver usando tipos específicos no seu código e estiver trabalhando em uma linguagem compilada ou garantir que uma verificação de análise estática seja executada com frequência, por exemplo, como parte do IC.
precisa saber é
6

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

Fırat KÜÇÜK
fonte
Lembre-se de que alguns getters ou setters têm efeitos colaterais (embora na maioria dos casos seja desencorajado e considerado uma má prática), portanto, a alteração no código-fonte também pode afetá-lo.
Andrzej Bobak
3

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

Tudo bem controverso, mas eu diria que quem responde '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 é, então não é uma resposta plausível.

O DDD no TDD

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

  • Defesa
    • Garantir que o código possa mudar, mas não seu comportamento .
    • Isso permite a sempre tão importante prática de refatoração .
    • Você ganha esse TDD ou não.
  • desenhar
    • Você especifica o que algo deve fazer, como deve se comportar antes de implementá- lo.
    • Isso geralmente significa decisões de implementação mais informadas .
  • Documentação
    • O conjunto de testes deve servir como documentação de 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 em um significa uma alteração em outro. Compare com a manutenção de requisitos e design em um documento do Word separado.

Separe a responsabilidade da implementação

Como programadores, é terrivelmente tentador pensar nos atributos como algo de significância e getters e setter como uma espécie de sobrecarga.

Mas os 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 escrever que um objeto deve:

Permitir que seus clientes alterem seu estado

e

Permitir que seus clientes consultem seu estado

então 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 da documentação do TDD.

O fato de a eventual implementação ser trivial (atributo) e não trazer benefícios de defesa 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 cujos artefatos (documentação, código) geralmente são inconsistentes.

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

... e como o TDD resolve

É a parte da 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 com falha, e depois escrevemos o código que os deixou passar.

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

Por que você deve excluir setters e getter?

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

Então pergunte a si mesmo:

A pessoa que está escrevendo os testes para uma classe menciona getters e setter.

Como getters e setters são uma interface pública para uma classe, a resposta é obviamente sim , ou não haverá como 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 essa regra - funções que são detalhes claros da implementação e claramente não fazem parte do design do sistema.

Por exemplo, um 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 publicinterface que precise de ortografia no design do componente do sistema.

Izhaki
fonte
1

Quando se deparar com uma questão filosófica, volte aos requisitos de direção.

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

Ou é para produzir software da mais alta confiabilidade possível quase 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 relatado por desenvolvedor por mês. Reduzir para metade para um a cada dois meses libera apenas um orçamento de talvez um dia ou dois, e muitos testes extras provavelmente não reduzirão pela metade a taxa de defeitos. Portanto, não é mais uma simples vitória / vitória; você precisa justificá-lo com base no custo do defeito para o cliente.

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

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

soru
fonte
0

Sua resposta depende da sua filosofia (acredite que é Chicago vs Londres? Tenho certeza que alguém vai procurar). O júri ainda não se pronunciou sobre a abordagem mais econômica em termos de tempo (porque, afinal, esse é o maior fator que leva a dedicar menos tempo a correções).

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

anon
fonte
0

É uma pergunta complicada.

A rigor, eu diria que não é necessário. É melhor escrever testes de nível de sistema e unidade de estilo BDD para garantir que os requisitos de negócios funcionem conforme o esperado em cenários positivos e negativos.

Dito isto, se o seu método não for coberto por esses casos de teste, você deverá questionar por que ele existe em primeiro lugar e se é necessário ou se existem requisitos ocultos no código que não são refletidos na documentação ou nas histórias de usuários que deve ser codificado em um caso de teste no estilo BDD.

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

Supondo que as melhores práticas de teste estejam sendo seguidas, isso oferece muita cobertura, sem forçar os desenvolvedores a perder tempo tentando descobrir como obter cobertura adicional em códigos difíceis de exercer ou triviais, simplesmente por uma questão de cobertura.

Keith Brings
fonte
-1

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

É o pensamento-chave em termos de características / comportamentos, em vez de pensar em termos de métodos e classes. É claro que existe um método aqui para fornecer suporte a um ou mais recursos; no final, todo o seu código é testado, pelo menos todo o código é importante na sua base de códigos.

No seu cenário, provavelmente essa classe "manager" é redundante ou desnecessária (como todas as classes com um nome que contém a palavra "manager"), ou talvez não, mas parece um detalhe de implementação, provavelmente essa classe não merece uma unidade teste porque esta classe não possui nenhuma lógica comercial relevante. Provavelmente, você precisa dessa classe para que algum recurso funcione; o teste para esse recurso abrange essa classe; dessa forma, você pode refatorar essa classe e fazer um teste para verificar se o que importa, seus recursos, ainda funciona após o refator.

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

AlfredoCasado
fonte
-4

Isso me fez pensar, no entanto. Devemos nos esforçar para obter a maior cobertura de teste em%?

Sim, idealmente 100%, mas algumas coisas não podem ser testadas por unidade.

getters e setters (a menos que eles realmente tenham alguma lógica neles)

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.

código "clichê"

Obtenha código comum e teste-o por unidade. Isso deve ser tão simples quanto isso.

Existem razões racionais / não "inflamáveis" pelas quais alguém deve testar todas as linhas de código (ou tantas quanto puder)?

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

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

BЈовић
fonte
Bem, vamos esclarecer uma coisa: eu não quis dizer que não uso TDD / testes de gravação. Muito pelo contrário. Sei que os testes podem encontrar erros nos quais não pensei, mas o que há para testar aqui? Simplesmente acho que esse método é um dos métodos "não testáveis ​​por unidade". Como Péter Török disse (citando Kent Beck), você deve testar coisas que podem quebrar. O que poderia quebrar aqui? Não é muita coisa (existe apenas uma delegação simples neste método). POSSO escrever um teste de unidade, mas ele simplesmente terá uma zombaria do DAO e uma afirmação, não muito teste. Quanto aos getters / setters, alguns frameworks os exigem.
Zenzen 19/01/12
11
Além disso, desde que eu não notei "Obtenha código comum e teste-o por unidade. Isso deve ser tão simples quanto isso". O que você quer dizer com isso? É uma classe de serviço (em uma camada de serviço entre a GUI e o DAO), é comum a todo o aplicativo. Não é possível torná-lo mais genérico (pois ele aceita alguns parâmetros e chama um determinado método no DAO). A única razão disso é aderir à arquitetura em camadas do aplicativo para que a GUI não chame o DAO diretamente.
Zenzen 19/01/12
20
-1 para "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". - Muito errado. Isso foi discutido várias vezes no SO . Usar campos públicos em qualquer lugar é realmente pior do que usar getters e setters em qualquer lugar.
Péter Török