Há algum ponto nos testes de unidade que entopam e zombam de tudo público?

56

Ao fazer testes unitários de maneira "correta", isto é, arrancar todas as chamadas públicas e retornar valores predefinidos ou simulações, sinto que não estou realmente testando nada. Estou literalmente olhando para o meu código e criando exemplos baseados no fluxo da lógica através dos meus métodos públicos. E toda vez que a implementação muda, eu tenho que mudar esses testes, novamente, não sentindo realmente que estou realizando algo útil (seja a médio ou longo prazo). Eu também faço testes de integração (incluindo caminhos não felizes) e realmente não me importo com o aumento dos tempos de teste. Com esses, eu sinto que estou realmente testando por regressões, porque eles pegaram vários, enquanto todos 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 eu sinto que não entendo nada aqui. Qual é a vantagem decisiva do teste de unidade versus teste de integração (excluindo a sobrecarga de tempo)?

    
por enthrops 17.05.2013 / 03:11
fonte

5 respostas

35

When doing unit tests the "proper" way, i.e. stubbing every public call and return preset values or mocks, I feel like I'm not actually testing anything. I'm literally looking at my code and creating examples based on the flow of logic through my public methods.

Isso soa como o método que você está testando precisa de várias outras instâncias de classe (que você tem que simular), e chama vários métodos por conta própria.

Esse tipo de código é realmente difícil de testar em unidade, pelas razões que você delineia.

O que achei útil é dividir essas classes em:

  1. Classes com a "lógica de negócios" real. Estes usam poucas ou nenhumas chamadas para outras classes e são fáceis de testar (valor (es) in-value out).
  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. Turmas que "unem tudo"

Em seguida, as classes de 1. são fáceis de testar na 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 só chamarão classes de 2. (e não chamarão diretamente, por exemplo, uma função de banco de dados), e as classes de 2. serão fáceis de burlar (porque expor as partes do sistema envolvido que você precisa).

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

Um exemplo

Uma classe

Digamos que você tenha uma classe que recupere um preço de um banco de dados, aplique alguns descontos e atualize o banco de dados.

Se você tiver tudo isso em uma classe, precisará chamar funções de banco de dados, que são difíceis de burlar. 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, muito de zombaria (complexa), que provavelmente será interrompida se o código ou a estrutura do banco de dados mudar.

Dividir

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

O PriceCalculation só faz 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 forma:

  • PriceCalculation encapsula a "lógica de negócios". É fácil de testar porque não chama nada por conta própria.
  • O PriceRepository pode ser testado por pseudo-unidade configurando um banco de dados simulado e testando as chamadas de leitura e atualização. Tem pouca lógica, portanto, poucos caminhos de código, então você não precisa de muitos desses testes.
  • O aplicativo não pode ser significativamente testado por unidade porque é um código de cola. No entanto, também é muito simples, portanto, o teste de integração deve ser suficiente. Se, posteriormente, o aplicativo ficar muito complexo, você criará mais classes de "lógica de negócios".

Finalmente, pode ser que o PriceCalculation faça suas próprias chamadas de banco de dados. Por exemplo, porque somente o PriceCalculation sabe quais dados são necessários, portanto, não pode ser obtido antecipadamente pelo App. Então você pode passar uma instância do PriceRepository (ou alguma outra classe de repositório), customizada para as necessidades do PriceCalculation. Essa classe precisará ser ridicularizada, mas isso será simples, porque a interface do PriceRepository é simples, por exemplo, %código%. Mais importante ainda, a interface do PriceRepository isola o PriceCalculation do banco de dados, portanto, é improvável que as mudanças no esquema do banco de dados ou na organização de dados alterem sua interface e, portanto, interrompam as falsificações.

    
por 22.05.2013 / 08:53
fonte
27

What's the decisive advantage of unit testing vs integration testing?

Essa é uma falsa dicotomia.

Teste de unidade e teste de integração atendem a duas finalidades semelhantes, mas diferentes. O objetivo do teste de unidade é garantir que seus métodos funcionem. Em termos práticos, os testes unitários garantem que o código cumpre o contrato delineado pelos testes unitários. Isto é evidente na forma como os testes unitários são projetados: eles especificam o que o código deve fazer e asseverar que o código faz isso.

Testes de integração são diferentes. Testes de integração exercitam a interação entre componentes de software. Você pode ter componentes de software que passam todos os testes e ainda falharem nos testes de integração, porque não interagem adequadamente.

No entanto, se houver uma vantagem decisiva nos testes unitários, é isso: 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 corretamente, testes unitários encorajam 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 tem certas características, como uma API coerente, comportamento repetível e retorna resultados fáceis de afirmar.

Testes de integração são mais difíceis e mais caros, porque muitas vezes você precisa de zombarias elaboradas, configurações complexas e asserções difíceis. No nível mais alto de integração do sistema, imagine tentar simular a interação humana em uma interface do usuário. Sistemas inteiros de software são dedicados a esse tipo de automação. E é a automação que estamos procurando; o teste humano não é repetitivo e não é dimensionado como o teste automatizado.

Por fim, 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 informarão quanto de cobertura de código você tem, e qual é a complexidade ciclomática do seu código. Mas eles só funcionam bem no nível do método, onde os testes de unidade vivem.

Se os seus testes estão mudando a cada vez que você refatorar, isso é um problema diferente. Testes de unidade devem ser sobre como documentar o que seu software faz, provando que isso é feito e, em seguida, provando que isso é feito novamente quando você refatora a implementação subjacente. Se a sua API mudar ou você precisar que seus métodos sejam alterados de acordo com uma alteração no design do sistema, é o que deve acontecer. Se estiver acontecendo muito, considere escrever seus testes primeiro, antes de escrever o código. Isso forçará você a pensar sobre a arquitetura geral e permitirá escrever código com a API já estabelecida.

Se você está gastando muito tempo escrevendo testes de unidade para código trivial 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, já que essa propriedade quase certamente será referida em algum outro lugar em seu 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 dependências de algo fora de si mesmo e agora é mais testável, pois é completamente autocontido. Concedido, você nem sempre poderá fazer isso, mas 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.

    
por 17.05.2013 / 05:16
fonte
4

Os testes de unidade com simulação 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 que é externo à classe e tem certeza de que um teste com falha se deve a algo interno da 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ê está mudando o que o método público faz, então os testes devem falhar, porque você mudou o comportamento.

Também parece que você está escrevendo os testes depois de ter escrito o código, tente escrever os testes primeiro. Tente delinear 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 corretamente. Eu quero ter os dois tipos na minha suíte de testes.

Testes de unidade me ajudaram no meu desenvolvimento, já que posso me concentrar em uma parte do aplicativo por 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).

UPDATE

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

O teste para este método nunca pode mudar ou pode mudar somente quando você está alterando os métodos. Por que isso é uma coisa ruim? Isso ajuda a reforçar o uso dos testes. Se você tiver que corrigir um teste depois de alterar o código, você terá o hábito de alterar os testes com o código.

    
por 17.05.2013 / 03:32
fonte
2

Eu estava passando por uma questão semelhante - até que descobri o poder dos testes de componentes. Em suma, eles são os mesmos que os 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 seus mocks o tempo todo. Pode ser um pouco menos preciso do que os testes de unidade com 100% de zombaria, mas o tempo e o dinheiro que você economiza compensam isso. A única coisa que você realmente precisa para usar mocks ou fixtures são back-end de armazenamento ou serviços externos.

Na verdade, a zombaria excessiva é um anti-padrão: Anti-padrões TDD e As zombarias são más .

    
por 21.11.2015 / 15:39
fonte
0

Embora o op já tenha marcado uma resposta, estou apenas adicionando meus 2 centavos aqui.

What's the decisive advantage of unit testing vs integration testing (excluding the time overhead)?

E também em resposta a

When doing unit tests the "proper" way, i.e. stubbing every public call and return preset values or mocks, I feel like I'm not actually testing anything.

Existe uma ajuda, mas não exatamente o que o OP pediu:

Testes de unidade funcionam mas ainda existem bugs?

da minha pequena experiência em suítes de testes, eu entendo que os Testes de Unidade são sempre para testar a funcionalidade de nível de método mais básica de uma classe. Na minha opinião, cada método público, privado ou interno merece ter testes unitários dedicados. Mesmo na minha experiência recente, eu tinha um método público que estava chamando outro pequeno método privado. Então, 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ê pensar logicamente, o objetivo de ter um método privado é: o método público principal está ficando muito grande ou bagunçado. Para resolver isso, você refatorou sabiamente e criou pequenos pedaços de código que merecem ser métodos privados separados que, por sua vez, tornam seu principal método público menos volumoso. Você refatora, tendo em mente que esse método privado pode ser reutilizado mais tarde. 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.

Então, se eu tivesse escolhido a abordagem 1: eu teria testes de unidade duplicados e eles teriam sido complicados, já que você tinha o número de testes de unidade para cada ramo do método público, assim como o método privado.

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

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

Quanto a testes de integração , eles tendem a ser exaustivos e mais de alto nível. Eles irão dizer-lhe 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.

    
por 07.08.2013 / 14:42
fonte