Existe um ponto em testes de unidade que stub e zombam de tudo público?


59

Ao fazer testes de unidade da maneira "adequada", ou seja, apagar todas as chamadas públicas e retornar valores predefinidos ou zombarias, sinto que não estou testando nada. Estou literalmente olhando meu código e criando exemplos com base no fluxo da lógica por meio de meus métodos públicos. E toda vez que a implementação muda, preciso alterar esses testes novamente, sem realmente sentir que estou conseguindo algo útil (seja a médio ou longo prazo). Também faço testes de integração (incluindo caminhos não felizes) e realmente não me importo com o aumento do tempo de teste. Com isso, sinto que estou realmente testando regressões, porque elas capturaram várias, enquanto tudo o que os testes de unidade fazem é me mostrar que a implementação do meu método público mudou, o que eu já sei.

O teste de unidade é um tópico vasto e sinto que não entendo algo aqui. Qual é a vantagem decisiva do teste de unidade versus teste de integração (excluindo a sobrecarga de tempo)?


4
Meus dois centavos: não use
Juan Mendes

Respostas:


37

Ao fazer testes de unidade da maneira "adequada", ou seja, apagar todas as chamadas públicas e retornar valores predefinidos ou zombarias, sinto que não estou testando nada. Estou literalmente olhando meu código e criando exemplos com base no fluxo da lógica por meio de meus métodos públicos.

Parece que o método que você está testando precisa de várias outras instâncias de classe (que você precisa zombar) e chama vários métodos por conta própria.

Esse tipo de código é realmente difícil de testar por unidade, pelos motivos que você descreve.

O que eu achei útil é dividir essas classes em:

  1. Classes com a "lógica de negócios" real. Eles usam poucas ou nenhuma chamada para outras classes e são fáceis de testar (valor (es) dentro - valor fora).
  2. Classes que fazem interface com sistemas externos (arquivos, banco de dados etc.). Eles envolvem o sistema externo e fornecem uma interface conveniente para suas necessidades.
  3. Classes que "unem tudo"

Então as classes de 1. são fáceis de testar por unidade, porque elas apenas aceitam valores e retornam um resultado. Em casos mais complexos, essas classes podem precisar executar chamadas por conta própria, mas chamarão apenas as classes a partir de 2. (e não chamarão diretamente, por exemplo, uma função de banco de dados), e as classes a partir de 2. são fáceis de zombar (porque apenas expor as partes do sistema agrupado necessárias).

As classes de 2. e 3. geralmente não podem ser significativamente testadas por unidade (porque elas não fazem nada útil por si mesmas, são apenas códigos de "cola"). OTOH, essas classes tendem a ser relativamente simples (e poucas), portanto devem ser adequadamente cobertas por testes de integração.


Um exemplo

Uma classe

Digamos que você tenha uma classe que recupera um preço de um banco de dados, aplica alguns descontos e atualiza o banco de dados.

Se você tiver tudo isso em uma classe, precisará chamar funções de banco de dados, difíceis de zombar. No pseudocódigo:

1 select price from database
2 perform price calculation, possibly fetching parameters from database
3 update price in database

Todas as três etapas precisarão de acesso ao banco de dados, portanto, muitas zombarias (complexas), que provavelmente serão interrompidas se o código ou a estrutura do banco de dados mudar.

Dividir

Você divide em três classes: PriceCalculation, PriceRepository, App.

O PriceCalculation faz apenas o cálculo real e recebe os valores necessários. App une tudo:

App:
fetch price data from PriceRepository
call PriceCalculation with input values
call PriceRepository to update prices

Dessa maneira:

  • PriceCalculation encapsula a "lógica de negócios". É fácil testar porque não chama nada por si só.
  • O PriceRepository pode ser testado por pseudo-unidade configurando um banco de dados falso e testando as chamadas de leitura e atualização. Ele tem pouca lógica e, portanto, poucos caminhos de código, portanto você não precisa de muitos desses testes.
  • O aplicativo não pode ser significativamente testado em unidade, porque é um código de cola. No entanto, também é muito simples, portanto, o teste de integração deve ser suficiente. Se o aplicativo posterior se tornar muito complexo, você quebrará mais classes de "lógica de negócios".

Por fim, pode acontecer que o PriceCalculation faça suas próprias chamadas ao banco de dados. Por exemplo, porque apenas o PriceCalculation sabe quais dados são necessários e, portanto, não pode ser buscado antecipadamente pelo aplicativo. Em seguida, você pode transmitir uma instância do PriceRepository (ou alguma outra classe de repositório), personalizada de acordo com as necessidades do PriceCalculation. Essa classe precisará ser ridicularizada, mas isso será simples, porque a interface do PriceRepository é simples, por exemplo PriceRepository.getPrice(articleNo, contractType). Mais importante ainda, a interface do PriceRepository isola o PriceCalculation do banco de dados, portanto, é improvável que as alterações no esquema do banco de dados ou na organização de dados alterem sua interface e, portanto, quebrem as zombarias.


5
Eu pensei que estava sozinho, não vendo o ponto em testar a unidade de tudo, obrigado
entradas

4
Apenas discordo quando você diz que as classes do tipo 3 são poucas, sinto que a maior parte do meu código é do tipo 3 e quase não existe lógica de negócios. Isto é o que eu quero dizer: stackoverflow.com/questions/38496185/...
Rodrigo Ruiz

27

Qual é a vantagem decisiva do teste de unidade versus teste de integração?

Essa é uma falsa dicotomia.

O teste de unidade e o teste de integração têm dois objetivos semelhantes, mas diferentes. O objetivo do teste de unidade é garantir que seus métodos funcionem. Em termos práticos, os testes de unidade garantem que o código cumpra o contrato descrito pelos testes de unidade. Isso é evidente na maneira como os testes de unidade são projetados: eles especificam especificamente o que o código deve fazer e afirmam que o código faz isso.

Os testes de integração são diferentes. Os testes de integração exercitam a interação entre os componentes de software. Você pode ter componentes de software que são aprovados em todos os testes e ainda falham nos testes de integração porque eles não interagem corretamente.

No entanto, se houver uma vantagem decisiva nos testes de unidade, é esta: os testes de unidade são muito mais fáceis de configurar e exigem muito menos tempo e esforço do que os testes de integração. Quando usados ​​adequadamente, os testes de unidade incentivam o desenvolvimento de código "testável", o que significa que o resultado final será mais confiável, mais fácil de entender e mais fácil de manter. O código testável possui certas características, como uma API coerente, comportamento repetível e retorna resultados fáceis de afirmar.

Os testes de integração são mais difíceis e mais caros, porque muitas vezes você precisa de zombaria elaborada, configuração complexa e afirmações difíceis. No nível mais alto de integração de sistemas, imagine tentar simular a interação humana em uma interface do usuário. Sistemas de software inteiros são dedicados a esse tipo de automação. E é a automação que estamos buscando; o teste em humanos não é repetível e não é dimensionado como o teste automatizado.

Finalmente, o teste de integração não garante a cobertura do código. Quantas combinações de loops de código, condições e ramificações você está testando com seus testes de integração? Você realmente sabe? Existem ferramentas que você pode usar com testes de unidade e métodos em teste que lhe dirão quanta cobertura de código você possui e qual é a complexidade ciclomática do seu código. Mas eles realmente funcionam bem no nível do método, onde vivem os testes de unidade.


Se seus testes estão mudando toda vez que você refatorar, isso é um problema diferente. Os testes de unidade devem documentar o que o software faz, provar que ele faz isso e depois provar que ele faz isso novamente quando você refatorar a implementação subjacente. Se sua API for alterada ou você precisar que seus métodos sejam alterados de acordo com uma alteração no design do sistema, é isso que deve acontecer. Se isso estiver acontecendo muito, considere escrever seus testes primeiro, antes de escrever o código. Isso forçará você a pensar na arquitetura geral e permitirá que você escreva código com a API já estabelecida.

Se você está gastando muito tempo escrevendo testes de unidade para códigos triviais como

public string SomeProperty { get; set; }

então você deve reexaminar sua abordagem. O teste de unidade deve testar o comportamento e não há comportamento na linha de código acima. No entanto, você criou uma dependência em seu código em algum lugar, pois essa propriedade quase certamente será mencionada em outra parte do código. Em vez de fazer isso, considere escrever métodos que aceitem a propriedade necessária como parâmetro:

public string SomeMethod(string someProperty);

Agora, seu método não tem nenhuma dependência de algo fora de si e agora é mais testável, pois é completamente independente. Concedido, você nem sempre poderá fazer isso, mas ele move seu código na direção de ser mais testável e, desta vez, você está escrevendo um teste de unidade para o comportamento real.


2
Estou ciente de que o teste de unidade e o servidor de teste de integração têm objetivos diferentes; no entanto, ainda não entendo como os testes de unidade são úteis se você stub e zombar de todas as chamadas públicas que os testes de unidade fazem. Eu entenderia que 'o código cumpre o contrato descrito pelos testes de unidade', se não fosse para stubs e zombarias; meus testes de unidade são literalmente reflexos da lógica dentro dos métodos que estou testando. Você (eu) não está realmente testando nada, apenas olhando para o seu código e 'convertendo' em testes. Com relação à dificuldade de automatizar e cobrir o código, atualmente estou fazendo o Rails, e esses dois são bem tratados.
entradas

2
Se seus testes são apenas reflexos da lógica do método, você está fazendo errado. Seus testes de unidade devem essencialmente entregar ao método um valor, aceitar um valor de retorno e fazer uma afirmação sobre qual deve ser esse valor de retorno. Nenhuma lógica é necessária para fazer isso.
Robert Harvey

2
Faz sentido, mas ainda precisa stubar todas as outras chamadas públicas (db, algumas 'globais', como status atual do usuário etc.) e terminar com o código de teste seguindo a lógica do método.
entradas

11
Então eu acho que os testes de unidade são principalmente para coisas isoladas que meio que confirmam 'conjunto de entradas -> conjunto de resultados esperados'?
entradas

11
Minha experiência na criação de muitos testes de unidade e integração (para não mencionar as ferramentas avançadas de zombaria, teste de integração e cobertura de código usadas por esses testes) contradiz a maioria de suas reivindicações aqui: 1) "O objetivo do teste de unidade é garantir que seu o código faz o que é suposto ": o mesmo se aplica ao teste de integração (ainda mais); 2) "testes de unidade são muito mais fáceis de configurar": não, eles não são (muitas vezes, são os testes de integração que são mais fáceis); 3) "Quando usados ​​corretamente, os testes de unidade incentivam o desenvolvimento de código" testável "": o mesmo com testes de integração; (continua)
Rogério

4

Os testes de unidade com zombarias são para garantir que a implementação da classe esteja correta. Você zomba das interfaces públicas das dependências do código que está testando. Dessa forma, você tem controle sobre tudo o que é externo à classe e tem certeza de que um teste com falha ocorre devido a algo interno à classe e não a um dos outros objetos.

Você também está testando o comportamento da classe em teste e não a implementação. Se você refatorar o código (criando novos métodos internos, etc.), os testes de unidade não devem falhar. Mas se você estiver alterando o que o método público faz, absolutamente os testes devem falhar porque você mudou o comportamento.

Também parece que você está escrevendo os testes depois de escrever o código; tente escrever os primeiros testes. Tente descrever o comportamento que a classe deve ter e, em seguida, escreva a quantidade mínima de código para fazer os testes passarem.

O teste de unidade e o teste de integração são úteis para garantir a qualidade do seu código. Os testes de unidade examinam cada componente isoladamente. E os testes de integração garantem que todos os componentes interajam adequadamente. Eu quero ter os dois tipos no meu conjunto de testes.

Os testes de unidade me ajudaram no meu desenvolvimento, pois posso me concentrar em uma parte do aplicativo de cada vez. Zombando dos componentes que ainda não fiz. Eles também são ótimos para regressão, pois documentam quaisquer erros na lógica que eu encontrei (mesmo nos testes de unidade).

ATUALIZAR

A criação de um teste que apenas garanta que os métodos sejam chamados tem valor, pois você garante que os métodos realmente sejam chamados. Especialmente se você estiver escrevendo seus testes primeiro, você tem uma lista de verificação dos métodos que precisam acontecer. Como esse código é bastante processual, você não precisa verificar muito além dos métodos chamados. Você está protegendo o código para alterações no futuro. Quando você precisar chamar um método antes do outro. Ou que um método sempre seja chamado, mesmo que o método inicial gere uma exceção.

O teste para esse método nunca pode ser alterado ou pode ser alterado apenas quando você está alterando os métodos. Por que isso é uma coisa ruim? Ajuda a reforçar o uso dos testes. Se você precisar corrigir um teste após alterar o código, terá o hábito de alterar os testes com o código.


E se um método não chama nada de privado, então não faz sentido testar a unidade, correto?
entradas

Se o método for privado, você não o testará explicitamente, ele deverá ser testado através da interface pública. Todos os métodos públicos devem ser testados, para garantir que o comportamento esteja correto.
Schleis 17/05

Não, quero dizer, se um método público não chama nada de privado, faz sentido testar esse método público?
entradas

Sim. O método faz alguma coisa, não é? Portanto, deve ser testado. Do ponto de vista dos testes, não sei se está usando algo particular. Eu só sei que se eu fornecer a entrada A, I deve obter uma saída B.
Schleis

Ah, sim, o método faz algo e esse algo é chamado para outros métodos públicos (e somente isso). Portanto, a maneira como você testaria 'adequadamente' o stub das chamadas com alguns valores de retorno e configuraria as expectativas da mensagem. O que EXATAMENTE você está testando neste caso? Que as chamadas certas são feitas? Bem, você escreveu esse método e pode vê-lo e ver exatamente o que ele faz. Eu acho que o teste de unidade é mais apropriado para métodos isolados que devem ser usados ​​como 'input -> output', para que você possa configurar vários exemplos e fazer testes de regressão quando refatorar.
entradas

3

Eu estava enfrentando uma pergunta semelhante - até descobrir o poder dos testes de componentes. Em resumo, eles são iguais aos testes de unidade, exceto que você não zomba por padrão, mas usa objetos reais (idealmente via injeção de dependência).

Dessa forma, você pode criar rapidamente testes robustos com uma boa cobertura de código. Não há necessidade de atualizar suas zombarias o tempo todo. Pode ser um pouco menos preciso do que testes de unidade com zombarias 100%, mas o tempo e o dinheiro que você economiza compensam isso. A única coisa para a qual você realmente precisa usar zombarias ou acessórios é back-end de armazenamento ou serviços externos.

Na verdade, a zombaria excessiva é um antipadrão: os antipadrões e zombarias TDD são maus .


0

Embora a operação já tenha marcado uma resposta, estou apenas adicionando meus 2 centavos aqui.

Qual é a vantagem decisiva do teste de unidade versus teste de integração (excluindo a sobrecarga de tempo)?

E também em resposta a

Ao fazer testes de unidade da maneira "adequada", ou seja, apagar todas as chamadas públicas e retornar valores predefinidos ou zombarias, sinto que não estou testando nada.

Existe um OP útil, mas não exatamente o que o OP pediu:

Os testes de unidade funcionam, mas ainda existem erros?

da minha pouca experiência em conjuntos de testes, entendo que os testes de unidade devem sempre testar a funcionalidade mais básica do nível de método de uma classe. Na minha opinião, todo método público, privado ou interno merece ter testes de unidade dedicados. Mesmo em minha experiência recente, eu tinha um método público que chamava outro pequeno método privado. Portanto, havia duas abordagens:

  1. não crie testes unitários para o método privado.
  2. crie um (s) teste (s) de unidade para um método privado.

Se você pensa logicamente, o objetivo de ter um método privado é: o principal método público está ficando muito grande ou bagunçado. Para resolver isso, você refatora sabiamente e cria pequenos trechos de código que merecem ser métodos privados separados que, por sua vez, tornam seu método público principal menos volumoso. Você refatora lembrando que esse método particular pode ser reutilizado posteriormente. Pode haver casos em que não há outro método público, dependendo desse método privado, mas quem sabe sobre o futuro.


Considerando o caso em que o método privado é reutilizado por muitos outros métodos públicos.

Portanto, se eu tivesse escolhido a abordagem 1: eu teria duplicado os testes de unidade e eles teriam sido complicados, pois você tinha um número de testes de unidade para cada ramo do método público e também do método privado.

Se eu tivesse escolhido a abordagem 2: o código escrito para testes de unidade seria relativamente menor e seria muito mais fácil de testar.


Considerando o caso em que o método privado não é reutilizado Não há sentido em escrever um teste de unidade separado para esse método.

Quanto aos testes de integração , eles tendem a ser exaustivos e mais de alto nível. Eles dirão a você que, dada uma entrada, todas as suas turmas devem chegar a esta conclusão final. Para entender mais sobre a utilidade do teste de integração, consulte o link mencionado.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.