(Por que) é importante que um teste unitário não teste dependências?

100

Eu entendo o valor do teste automatizado e o uso onde quer que o problema esteja bem especificado o suficiente para que eu possa criar bons casos de teste. Notei, no entanto, que algumas pessoas aqui e no StackOverflow enfatizam o teste somente de uma unidade, não de suas dependências. Aqui eu não vejo o benefício.

Mocking / stub para evitar dependências de teste adiciona complexidade aos testes. Adiciona requisitos artificiais de flexibilidade / desacoplamento ao seu código de produção para suportar o escárnio. (Não concordo com quem diz que isso promove um bom design. Escrever código extra, introduzir elementos como estruturas de injeção de dependência ou adicionar complexidade à sua base de código para tornar as coisas mais flexíveis / extensíveis / desacopladas sem um caso de uso real é excessivo. bom design.)

Em segundo lugar, testar dependências significa que o código crítico de baixo nível usado em todos os lugares é testado com entradas diferentes daquelas que quem escreveu seus testes foram explicitamente pensados. Eu encontrei muitos bugs na funcionalidade de baixo nível, executando testes de unidade em funcionalidades de alto nível sem zombar da funcionalidade de baixo nível da qual dependia. O ideal seria que esses testes de unidade tivessem sido encontrados para a funcionalidade de baixo nível, mas os casos perdidos sempre acontecem.

Qual é o outro lado disso? É realmente importante que um teste de unidade também não teste suas dependências? Se sim, por quê?

Edit: Eu posso entender o valor de escarnecer dependências externas como bancos de dados, redes, serviços web, etc. (Agradeço a Anna Lear por me motivar a esclarecer isso.) Eu estava me referindo a eles > dependências internas , isto é, outras classes, funções estáticas, etc., que não possuem dependências externas diretas.

    
por dsimcha 06.04.2011 / 03:05
fonte

11 respostas

113

É uma questão de definição. Um teste com dependências é um teste de integração, não um teste de unidade. Você também deve ter um conjunto de testes de integração. A diferença é que o conjunto de testes de integração pode ser executado em uma estrutura de teste diferente e provavelmente não como parte da compilação, porque eles demoram mais.

Para o nosso produto: Nossos testes de unidade são executados com cada compilação, levando segundos. Um subconjunto de nossos testes de integração é executado a cada check-in, levando 10 minutos. Nosso pacote de integração completo é executado a cada noite, levando 4 horas.

    
por 06.04.2011 / 03:11
fonte
37

O teste com todas as dependências no local ainda é importante, mas está mais no campo dos testes de integração, como Jeffrey Faust disse.

Um dos aspectos mais importantes do teste de unidade é tornar seus testes confiáveis. Se você não acredita que um teste de aprovação realmente significa que as coisas estão boas e que um teste com falha realmente significa um problema no código de produção, seus testes não são tão úteis quanto podem ser.

Para tornar seus testes confiáveis, você precisa fazer algumas coisas, mas vou me concentrar em apenas uma para essa resposta. Você precisa ter certeza de que são fáceis de executar, para que todos os desenvolvedores possam executá-los facilmente antes de verificar o código. "Fácil de executar" significa que seus testes são executados rapidamente e não há configuração extensiva ou configuração necessária para fazê-los ir. Idealmente, qualquer pessoa deve poder verificar a versão mais recente do código, executar os testes imediatamente e vê-los passar.

Abstrair as dependências de outras coisas (sistema de arquivos, banco de dados, serviços da Web, etc.) permite evitar a configuração e torna você e outros desenvolvedores menos suscetíveis a situações em que você é tentado a dizer "Oh, os testes falharam porque Eu não tenho o compartilhamento de rede configurado. Oh, bem, eu vou executá-los mais tarde ".

Se você quiser testar o que faz com alguns dados, seus testes de unidade para esse código de lógica de negócios não devem se preocupar com como você obtém esses dados. Ser capaz de testar a lógica do núcleo do seu aplicativo sem depender de material de suporte, como bancos de dados, é incrível. Se você não está fazendo isso, você está perdendo.

P.S. Eu devo acrescentar que é definitivamente possível overengineer em nome da testabilidade. Testar o seu aplicativo ajuda a minimizar isso. Mas em qualquer caso, implementações pobres de uma abordagem não tornam a abordagem menos válida. Qualquer coisa pode ser mal usada e exagerada se não ficarmos perguntando "por que estou fazendo isso?" enquanto se desenvolve.

No que diz respeito às dependências internas, as coisas ficam um pouco turvas. A maneira que eu gosto de pensar sobre isso é que eu quero proteger minha classe tanto quanto possível de mudar pelas razões erradas. Se eu tiver uma configuração como algo assim ...
public class MyClass 
{
    private SomeClass someClass;
    public MyClass()
    {
        someClass = new SomeClass();
    }

    // use someClass in some way
}

Eu geralmente não me importo como SomeClass é criado. Eu só quero usar isso. Se SomeClass mudar e agora requer parâmetros para o construtor ... isso não é problema meu. Eu não deveria ter que mudar o MyClass para acomodar isso.

Agora, isso é apenas tocar na parte do design. No que se refere aos testes de unidade, também quero me proteger de outras classes. Se eu estou testando o MyClass, eu gosto de saber que não há dependências externas, que a SomeClass não introduziu em algum momento uma conexão de banco de dados ou algum outro link externo.

Mas um problema ainda maior é que também sei que alguns dos resultados dos meus métodos dependem da saída de algum método no SomeClass. Sem zombar / apagar o SomeClass, talvez eu não tenha como variar essa entrada na demanda. Se eu tiver sorte, talvez consiga compor meu ambiente dentro do teste de modo a acionar a resposta certa da SomeClass, mas dessa forma introduz complexidade em meus testes e os torna frágeis.

Reescrevendo MyClass para aceitar uma instância de SomeClass no construtor permite-me criar uma instância falsa de SomeClass que retorna o valor que eu quero (seja através de um quadro de zombaria ou com uma simulação manual). Eu geralmente não tenho para introduzir uma interface neste caso. O que fazer ou não é, em muitos aspectos, uma escolha pessoal que pode ser ditada pelo idioma de sua escolha (por exemplo, as interfaces são mais prováveis em C #, mas você definitivamente não precisaria de uma em Ruby).

    
por 06.04.2011 / 03:21
fonte
21

Além do problema do teste de unidade versus integração, considere o seguinte.

O Class Widget tem dependências nas classes Thingamajig e WhatsIt.

Um teste de unidade para o Widget falha.

Em qual classe está o problema?

Se você respondeu "acionar o depurador" ou "ler o código até encontrá-lo", você entende a importância de testar apenas a unidade, não as dependências.

    
por 06.04.2011 / 03:28
fonte
14

Imagine que a programação é como cozinhar. Em seguida, o teste unitário é o mesmo que garantir que seus ingredientes estejam frescos, saborosos, etc. Considerando que o teste de integração é como garantir que sua refeição seja deliciosa.

Em última análise, certificar-se de que sua refeição é deliciosa (ou que seu sistema funciona) é a coisa mais importante, o objetivo final. Mas se seus ingredientes, quero dizer, unidades, trabalho, você terá uma maneira mais barata de descobrir.

De fato, se você pode garantir que suas unidades / métodos funcionem, é mais provável que você tenha um sistema em funcionamento. Eu enfatizo "mais provável", em oposição a "certo". Você ainda precisa de seus testes de integração, assim como você ainda precisa de alguém para provar a refeição que você cozinhou e dizer que o produto final é bom. Você terá mais facilidade em chegar lá com ingredientes frescos, isso é tudo.

    
por 06.04.2011 / 04:26
fonte
6

Testar as unidades isolando-as completamente permitirá testar TODAS as possíveis variações de dados e situações em que esta unidade pode ser submetida. Por estar isolado do resto, você pode ignorar variações que não tenham efeito direto na unidade a ser testada. isso, por sua vez, diminuirá muito a complexidade dos testes.

O teste com vários níveis de dependências integradas permitirá que você teste cenários específicos que podem não ter sido testados nos testes de unidade.

Ambos são importantes. Apenas fazendo teste de unidade, você invariavelmente irá ocorrer erros sutis mais complexos ao integrar os componentes. Apenas fazer testes de integração significa que você está testando um sistema sem a confiança de que as partes individuais da máquina não foram testadas. Pensar que você pode conseguir um teste completo é quase impossível, pois quanto mais componentes você adicionar, mais rapidamente o número de combinações de entradas aumentará (pense em fatorial) e a criação de um teste com cobertura suficiente se tornará impossível.

Então, resumindo, os três níveis de "teste de unidade" que eu geralmente uso em quase todos os meus projetos:

  • Testes de unidade, isolados com dependências ridicularizadas ou stubbed para testar o inferno de um único componente. O ideal seria tentar uma cobertura completa.
  • Testes de integração para testar os erros mais sutis. Verificações pontuais cuidadosamente criadas que mostrariam casos limite e condições típicas. Cobertura completa muitas vezes não é possível, esforço focado no que faria o sistema falhar.
  • Teste de especificação para injetar vários dados da vida real no sistema, instrumentar os dados (se possível) e observar a saída para garantir que ela esteja de acordo com as regras de especificação e de negócios.

Injeção de dependência é um meio muito eficiente para conseguir isso, pois permite isolar componentes para testes de unidade com muita facilidade, sem adicionar complexidade ao sistema. Seu cenário de negócios pode não justificar o uso do mecanismo de injeção, mas seu cenário de teste quase o fará. Isso para mim é suficiente para tornar então indispensável. Você também pode usá-los para testar os diferentes níveis de abstração independentemente por meio de testes de integração parcial.

    
por 06.04.2011 / 04:00
fonte
4

What's the other side to this? Is it really important that a unit test doesn't also test its dependencies? If so, why?

Unidade. Significa singular.

Testar 2 coisas significa que você tem as duas coisas e todas as dependências funcionais.

Se você adicionar uma terceira coisa, você aumenta as dependências funcionais nos testes além de linearmente. As interconexões entre as coisas crescem mais rapidamente do que o número de coisas.

Existem n (n-1) / 2 dependências potenciais entre os itens n testados.

Esse é o grande motivo.

Simplicidade tem valor.

    
por 06.04.2011 / 03:10
fonte
2

Lembre-se de como aprendeu a fazer recursão pela primeira vez? Meu prof disse "Suponha que você tenha um método que faz x" (por exemplo, resolve fibbonacci para qualquer x). "Para resolver x você tem que chamar esse método para x-1 e x-2". No mesmo sentido, eliminar as dependências permite que você finja que elas existem e teste se a unidade atual faz o que deve fazer. A suposição é claro que você está testando as dependências com rigor.

Este é essencialmente o SRP em ação. Concentrar-se em uma única responsabilidade, mesmo para os seus testes, elimina a quantidade de malabarismo mental que você precisa fazer.

    
por 06.04.2011 / 06:11
fonte
2

(Esta é uma resposta menor. Obrigado @TimWilliscroft pela dica.)

Falhas são mais fáceis de localizar se:

  • A unidade em teste e cada uma de suas dependências são testadas independentemente.
  • Cada teste falha se e somente se houver uma falha no código coberto pelo teste.

Isso funciona muito bem no papel. No entanto, conforme ilustrado na descrição do OP (as dependências são defeituosas), se as dependências não estão sendo testadas, seria difícil identificar a localização da falha.

    
por 06.04.2011 / 09:16
fonte
2

Muitas boas respostas. Eu também adicionaria alguns outros pontos:

O teste de unidade também permite que você teste seu código quando as dependências não existem . Por exemplo. você, ou sua equipe, ainda não escreveu as outras camadas, ou talvez você esteja esperando por uma interface fornecida por outra empresa.

Testes de unidade também significam que você não precisa ter um ambiente completo em sua máquina dev (por exemplo, um banco de dados, um servidor web, etc.). Eu recomendaria strongmente que todos os desenvolvedores façam esse ambiente, no entanto, o corte é, a fim de reproduzir erros etc. No entanto, se por algum motivo não for possível imitar o ambiente de produção, então unidade testar pelo menos lhe dá algum nível de confiança em seu código antes de entrar em um sistema de teste maior.

    
por 06.04.2011 / 10:32
fonte
0

Sobre os aspectos do design: Acredito que até projetos pequenos se beneficiam de tornar o código testável. Você não precisa necessariamente introduzir algo como Guice (uma classe de fábrica simples geralmente fará), mas separar o processo de construção da lógica de programação resulta em

  • documentando claramente as dependências de todas as classes através de sua interface (muito útil para pessoas que são novas na equipe)
  • as classes se tornam muito mais claras e fáceis de manter (uma vez que você coloca a criação do grafo de objetos em uma classe separada)
  • baixo acoplamento (facilitando as mudanças)
por 06.04.2011 / 09:45
fonte
0

Uh ... há bons pontos nos testes unitários e de integração escritos nestas respostas aqui!

Sinto falta das visualizações práticas e relacionadas a custos aqui. Dito isso, vejo claramente o benefício de testes unitários muito isolados / atômicos (talvez altamente independentes um do outro e com a opção de executá-los em paralelo e sem dependências, como bancos de dados, sistema de arquivos etc.) e testes de integração de nível mais alto. mas ... também é uma questão de custos (tempo, dinheiro, ...) e riscos .

Portanto, há outros fatores, que são muito mais importantes (como "o que testar" ) antes de pensar em "como testar" da minha experiência. .

Meu cliente (implicitamente) paga pela quantia extra de testes de escrita e manutenção? Uma abordagem mais voltada para testes (escrever testes primeiro antes de você escrever o código) é realmente econômica em meu ambiente (análise de risco / custo de falha de código, pessoas, especificações de projeto, configuração de ambiente de teste)? O código é sempre defeituoso, mas poderia ser mais econômico mover o teste para o uso de produção (no pior dos casos!)?

Também depende muito da qualidade do seu código (padrões) ou dos frameworks, IDE, princípios de design, etc. que você e sua equipe seguem e como eles são experientes. Um código bem documentado, facilmente compreensível, suficientemente documentado (idealmente auto-documentado), modular, ... apresenta provavelmente menos erros do que o oposto. Portanto, os custos / riscos reais de "necessidade", pressão ou manutenção geral / correções de erros para testes extensivos podem não ser altos.

Vamos levar isso ao extremo, onde um colega de trabalho da minha equipe sugeriu, nós temos que tentar testar a unidade do nosso código de camada de modelo Java EE puro com uma cobertura 100% desejada para todas as classes dentro e zombando do banco de dados. Ou o gerente que deseja que os testes de integração sejam cobertos com 100% de todos os possíveis casos de uso do mundo real e fluxos de trabalho da web, porque não queremos arriscar nenhum caso de uso a falhar. Mas temos um orçamento apertado de cerca de 1 milhão de euros, um plano bem apertado para codificar tudo. Um ambiente de cliente, onde potenciais bugs de aplicativos não seriam um perigo muito grande para seres humanos ou empresas. Nosso aplicativo será testado internamente com (alguns) testes de unidade importantes, testes de integração, testes importantes com planos de teste, uma fase de teste, etc. Não desenvolvemos um aplicativo para uma fábrica nuclear ou para a produção farmacêutica! (Nós temos apenas um banco de dados designado, fácil de testar clone para cada dev e strongmente acoplado à camada webapp ou modelo)

Eu próprio tento escrever teste se possível e desenvolvo enquanto testo o meu código. Mas muitas vezes faço isso de uma abordagem de cima para baixo (teste de integração) e tento encontrar o ponto positivo, onde fazer o "corte da camada de aplicativo" para os testes importantes (geralmente na camada de modelo). (porque muitas vezes é muito sobre as "camadas")

Além disso, o código de teste de unidade e integração não vem sem impacto negativo no tempo, dinheiro, manutenção, codificação etc. É legal e deve ser aplicado, mas com cuidado e considerando as implicações ao iniciar ou após 5 anos de muito de código de teste desenvolvido.

Então, eu diria que realmente depende muito da confiança e da avaliação custo / risco / benefício ... como na vida real, onde você não pode e não quer correr com um ou 100% de mecanismos de segurança.

O garoto pode e deve subir em algum lugar e pode cair e se machucar. O carro pode parar de funcionar porque eu preenchi o combustível errado (entrada inválida :)). A torrada pode ser queimada se o botão de tempo parar de funcionar após 3 anos. Mas eu não, nunca, quero dirigir na estrada e segurar meu volante de direção nas minhas mãos:)

    
por 16.01.2018 / 09:51
fonte