(Por que) é importante que um teste de unidade não teste dependências?

103

Entendo o valor do teste automatizado e o uso sempre que o problema é bem especificado o suficiente para que eu possa apresentar bons casos de teste. Notei, no entanto, que algumas pessoas aqui e no StackOverflow enfatizam o teste apenas de uma unidade, não de suas dependências. Aqui não vejo o benefício.

Mocking / stubbing para evitar dependências de teste adiciona complexidade aos testes. Ele adiciona requisitos artificiais de flexibilidade / desacoplamento ao seu código de produção para oferecer suporte à zombaria. (Eu discordo de qualquer um que diga que isso promove um bom design. Escrever código extra, introduzir coisas como estruturas de injeção de dependência ou adicionar complexidade à sua base de código para tornar as coisas mais flexíveis / conectáveis ​​/ extensíveis / desacopladas sem um caso de uso real é uma superengenharia. 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 em que quem escreveu seus testes pensou explicitamente. Encontrei muitos bugs na funcionalidade de baixo nível executando testes de unidade na funcionalidade de alto nível sem zombar da funcionalidade de baixo nível em que dependia. Idealmente, eles seriam encontrados pelos testes de unidade para a funcionalidade de baixo nível, mas 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 zombar de dependências externas, como bancos de dados, redes, serviços da web etc. (Obrigado Anna Lear por me motivar a esclarecer isso.) Eu estava me referindo a dependências internas , ou seja, outras classes, funções estáticas, etc. que não possuem dependências externas diretas.

dsimcha
fonte
14
"superengenharia, design não bom". Você precisará fornecer mais evidências do que isso. Muita gente não chamaria isso de "excesso de engenharia", mas de "melhores práticas".
S.Lott
9
@ S.Lott: Claro que isso é subjetivo. Eu só não queria um monte de respostas evitando o problema e dizendo que zombar é bom porque tornar seu código zombável promove um bom design. Geralmente, porém, odeio lidar com código dissociado de maneiras que não têm benefícios óbvios agora ou no futuro próximo. Se você não tem várias implementações agora e não espera tê-las no futuro próximo, então IMHO, você deve apenas codificá-lo. É mais simples e não aborrece o cliente com os detalhes das dependências de um objeto.
perfil completo de Dsimcha
5
Geralmente, porém, eu odeio lidar com código que é acoplado de maneiras que não têm lógica óbvia, exceto o design deficiente. É mais simples e não incomoda o cliente com a flexibilidade de testar as coisas isoladamente.
precisa saber é o seguinte
3
@ S.Lott: Esclarecimento: eu quis dizer um código que se esforça significativamente para desacoplar as coisas sem nenhum caso de uso claro, especialmente quando ele torna o código ou o código do cliente significativamente mais detalhado, introduz outra classe / interface, etc. é claro que não estou pedindo que o código seja mais fortemente associado ao design mais simples e conciso. Além disso, quando você cria linhas de abstração prematuramente, elas geralmente acabam nos lugares errados.
precisa
Considere atualizar sua pergunta para esclarecer qualquer distinção que você esteja fazendo. Integrar uma sequência de comentários é difícil. Por favor, atualizar a questão para esclarecer e concentrar-lo.
precisa saber é o seguinte

Respostas:

116

É 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 leva mais tempo.

Para o nosso produto: nossos testes de unidade são executados a cada compilação, levando segundos. Um subconjunto de nossos testes de integração é executado a cada check-in, levando 10 minutos. Nossa suíte de integração completa é executada todas as noites, levando 4 horas.

Jeffrey Faust
fonte
4
Não se esqueça dos testes de regressão para cobrir erros descobertos e corrigidos para impedir sua reintrodução em um sistema durante manutenção futura.
RBerteig 06/04
2
Eu não insistiria na definição, mesmo se eu concordasse com ela. Algum código pode ter dependências aceitáveis ​​como um StringFormatter e ainda é considerado pela maioria dos testes de unidade.
Danidacar 25/09
2
danip: definições claras são importantes, e eu iria insistir neles. Mas também é importante perceber que essas definições são um alvo. Vale a pena procurar, mas você nem sempre precisa de um alvo. Os testes de unidade geralmente dependem de bibliotecas de nível inferior.
Jeffrey Faust
2
Também é importante entender o conceito de testes de fachada, que não testam como os componentes se encaixam (como testes de integração), mas testam um único componente isoladamente. (. isto é, um gráfico de objeto)
Ricardo Rodrigues
1
não se trata apenas de ser mais rápido, mas de ser extremamente tedioso ou incontrolável, imagine, se não o fizer, sua árvore de zombaria de execução pode estar crescendo exponencialmente. Imagine que você zomba de 10 dependências em sua unidade, se você pressionar ainda mais a zombaria, digamos que 1 dessas 10 dependências seja usada em muitos lugares e tenha 20 de suas próprias dependências, então você deve zombar de + 20 outras dependências, potencialmente duplicada em muitos lugares. no ponto final da API, você precisa zombar - por exemplo, banco de dados, para que você não precise redefini-lo e seja mais rápido, como você disse
FantomX1 06/10
39

Testar com todas as dependências existentes 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 de 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 esta resposta. Você precisa garantir que eles sejam fáceis de executar, para que todos os desenvolvedores possam executá-los facilmente antes de fazer o check-in do código. "Fácil de executar" significa que seus testes são executados rapidamente e não é necessária nenhuma configuração ou configuração extensa para fazê-los funcionar. 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 nas quais você é tentado a dizer "Ah, os testes falharam porque eu não" não tenho o compartilhamento de rede configurado. Oh, bem. Vou executá-los mais tarde. "

Se você deseja 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 importar com a forma como você obtém esses dados. Ser capaz de testar a lógica principal do seu aplicativo sem depender de itens como bancos de dados é impressionante. Se você não está fazendo isso, está perdendo.

PS Devo acrescentar que é definitivamente possível fazer engenharia em nome da testabilidade. Testar o aplicativo ajuda a atenuar isso. Mas, de qualquer forma, implementações ruins de uma abordagem não tornam a abordagem menos válida. Qualquer coisa pode ser mal usada e exagerada se não continuar perguntando "por que estou fazendo isso?" durante o desenvolvimento.


No que diz respeito às dependências internas, as coisas ficam um pouco confusas. O jeito que eu gosto de pensar é que quero proteger minha classe o máximo possível de mudanças 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 usá-lo. Se SomeClass mudar e agora exigir parâmetros ao construtor ... isso não é problema meu. Eu não deveria ter que mudar o MyClass para acomodar isso.

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

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

Reescrever MyClass para aceitar uma instância de SomeClass no construtor me permite criar uma instância falsa de SomeClass que retorna o valor que eu quero (por meio de uma estrutura de simulação ou com uma simulação manual). Geralmente, não preciso introduzir uma interface neste caso. Fazer isso ou não é, sob muitos aspectos, uma escolha pessoal que pode ser ditada pelo seu idioma preferido (por exemplo, as interfaces são mais prováveis ​​em C #, mas você definitivamente não precisaria de uma em Ruby).

Adam Lear
fonte
+1 Ótima resposta, porque dependências externas são um caso em que eu não pensava quando escrevi a pergunta original. Esclarei minha pergunta em resposta. Veja a edição mais recente.
perfil completo de Dsimcha
@dsimcha Expandi minha resposta para entrar em mais detalhes sobre isso. Espero que ajude.
Adam Lear
Adam, você pode sugerir um idioma em que "Se SomeClass mudar e agora exigir parâmetros para o construtor ... eu não precise alterar MyClass" for verdadeiro? Desculpe, isso apenas me chamou a atenção como um requisito incomum. Percebo que não é necessário alterar os testes de unidade do MyClass, mas não é necessário alterar o MyClass ... uau.
1
@moz O que eu quis dizer foi que, da maneira como o SomeClass é criado, o MyClass não precisa mudar se o SomeClass for injetado nele, em vez de criado internamente. Em circunstâncias normais, o MyClass não precisa se preocupar com os detalhes de configuração do SomeClass. Se a interface de SomeClass mudar, sim ... MyClass ainda precisará ser modificado se algum dos métodos usados ​​for afetado.
Adam Lear
1
Se você zombar de SomeClass ao testar o MyClass, como detectará se o MyClass está usando o SomeClass incorretamente ou se baseia em uma peculiaridade não testada do SomeClass que pode mudar?
Nome
22

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

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

Um teste de unidade para o widget falha.

Em que classe está o problema?

Se você respondeu "inicie o depurador" ou "leia o código até que eu o encontre", entenderá a importância de testar apenas a unidade, não as dependências.

Ribeiro
fonte
3
@ Brook: Que tal ver quais são os resultados para todas as dependências do Widget? Se todos eles forem aprovados, é um problema com o Widget até que se prove o contrário.
amigos estão dizendo sobre dsimcha
4
@dsimcha, mas agora você está adicionando complexidade de volta ao jogo para verificar as etapas intermediárias. Por que não simplificar e apenas fazer testes simples de unidade primeiro? Então faça seu teste de integração.
asoundmove
1
@dsimcha, isso parece razoável até você entrar em um gráfico de dependência não trivial. Digamos que você tem um objeto complexo que tem 3+ camadas profundas com dependências, ele se transforma em um O (n ^ 2) pesquisa para o problema, em vez de O (1)
Brook
1
Além disso, minha interpretação do SRP é que uma classe deve ter uma única responsabilidade no nível conceitual / de domínio do problema. Não importa se o cumprimento dessa responsabilidade exige que ele faça muitas coisas diferentes quando visualizado em um nível mais baixo de abstração. Se levado ao extremo, o SRP seria contrário ao OO, porque a maioria das classes mantém dados e realiza operações nele (duas coisas quando vistas em um nível suficientemente baixo).
dsimcha
4
@ Brook: Mais tipos não aumentam a complexidade em si. Mais tipos é bom se esses tipos fizerem sentido conceitual no domínio do problema e facilitarem o código, não mais difícil de entender. O problema é desacoplar artificialmente coisas que são acopladas conceitualmente no nível do domínio do problema (ou seja, você só tem uma implementação, provavelmente nunca terá mais de uma, etc.) e a dissociação não mapeia bem os conceitos do domínio do problema. Nesses casos, é bobagem, burocrática e detalhada criar abstrações sérias em torno dessa implementação.
dsimcha
14

Imagine que a programação é como cozinhar. Então o teste de unidade é o mesmo que garantir que seus ingredientes sejam frescos, saborosos, etc. Considerando que o teste de integração é como garantir que sua refeição seja deliciosa.

Por fim, garantir que sua refeição seja deliciosa (ou que seu sistema funcione) é a coisa mais importante, o objetivo final. Mas se seus ingredientes, unidades, funcionarem, 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. Enfatizo "mais provável", em oposição a "certo". Você ainda precisa de seus testes de integração, assim como ainda precisa de alguém para provar a refeição que preparou e dizer que o produto final é bom. Você terá mais facilidade para chegar lá com ingredientes frescos, só isso.

Julio
fonte
7
E quando seus testes de integração falham, o teste de unidade pode se concentrar no problema.
Tim Williscroft
9
Para fazer uma analogia: quando você acha que sua refeição está horrível, pode ser necessário investigar para determinar se foi porque seu pão está mofado. O resultado, porém, é que você adiciona um teste de unidade para verificar se há pão mofado antes de cozinhar, para que esse problema específico não possa ocorrer novamente. Então, se você tiver outra falha na refeição mais tarde, você eliminou o pão mofado como causa.
21711 Kyralessa
Amo essa analogia!
Thehowler 17/05
6

Testar 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 afetam diretamente a unidade a ser testada. isso, por sua vez, diminuirá bastante a complexidade dos testes.

Testar com vários níveis de dependências integrados permitirá testar cenários específicos que podem não ter sido testados nos testes de unidade.

Ambos são importantes. Ao fazer o teste de unidade, você invariavelmente ocorrerá erros sutis mais complexos ao integrar 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 obter um teste completo melhor apenas realizando testes de integração é quase impossível, pois quanto mais componentes você adicionar, o número de combinações de entradas cresce MUITO rápido (pense em fatorial) e criar um teste com cobertura suficiente se torna rapidamente impossível.

Em resumo, os três níveis de "teste de unidade" que normalmente uso em quase todos os meus projetos:

  • Testes de unidade, isolados com dependências simuladas 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 elaboradas que mostrariam casos limite e condições típicas. A cobertura completa geralmente não é possível, um 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 esteja em conformidade com as regras de especificação e de negócios.

A injeção de dependência é um meio muito eficiente de conseguir isso, pois permite isolar componentes para testes de unidade com muita facilidade, sem adicionar complexidade ao sistema. Seu cenário comercial pode não garantir o uso do mecanismo de injeção, mas seu cenário de teste quase o fará. Isso para mim é suficiente para torná-lo 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.

Newtopian
fonte
4

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

Unidade. Significa singular.

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

Se você adicionar uma terceira coisa, aumentará 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 n itens sendo testados.

Essa é a grande razão.

Simplicidade tem valor.

S.Lott
fonte
2

Lembra como você aprendeu a fazer recursão? Meu professor disse: "Suponha que você tenha um método que faça x" (por exemplo, resolve fibbonacci para qualquer x). "Para resolver x, você precisa chamar esse método para x-1 e x-2". Da mesma forma, remover dependências permite fingir que existem e testar se a unidade atual faz o que deve fazer. A suposição é, obviamente, que você está testando as dependências com o mesmo rigor.

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

Michael Brown
fonte
2

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

As 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 incorretas), se as dependências não estiverem sendo testadas, seria difícil identificar a localização da falha.

rwong
fonte
Por que as dependências não seriam testadas? Eles são presumivelmente de nível mais baixo e mais fáceis de testar na unidade. Além disso, com os testes de unidade que cobrem código específico, é mais fácil identificar a localização da falha.
18711 David Thornley #
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 não existem dependências . Por exemplo, você ou sua equipe ainda não escreveu as outras camadas ou talvez esteja esperando 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 de desenvolvimento (por exemplo, um banco de dados, um servidor web, etc.). Eu forte recomendar que todos os desenvolvedores fazer ter um ambiente tal, no entanto reduzir o que é, a fim de erros repro etc. No entanto, se por algum motivo, não é possível para imitar o ambiente de produção, em seguida, teste de unidade, pelo menos dá yu algum nível de confiança no seu código antes de entrar em um sistema de teste maior.

Steve
fonte
0

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

  • documentar 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 feia criação de gráficos de objetos em uma classe separada)
  • acoplamento solto (facilitando muito as alterações)
Oliver Weiler
fonte
2
Método: Sim, mas você precisa adicionar código e classes extras para fazer isso. O código deve ser o mais conciso possível enquanto ainda estiver legível. O IMHO adicionando fábricas e outros enfeites é uma superengenharia, a menos que você possa provar que precisa ou é muito provável que precise da flexibilidade que ela fornece.
dsimcha
0

Uh ... existem bons pontos nos testes de unidade e integração escritos nestas respostas aqui!

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

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

Meu cliente (implicitamente) paga pela quantia extra de gravação e manutenção de testes? Uma abordagem mais orientada a testes (escrever testes antes de escrever o código) é realmente econômica em meu ambiente (risco de falha de código / análise de custos, pessoas, especificações de design, configuração de ambiente de teste)? O código é sempre incorreto, mas poderia ser mais econômico mover o teste para o uso da produção (na pior das hipóteses!)?

Também depende muito de qual é a qualidade do seu código (padrões) ou das estruturas, IDE, princípios de design etc. que você e sua equipe seguem e qual a experiência deles. Um código bem escrito, facilmente compreensível, bem documentado (idealmente auto-documentado), modular, ... introduz provavelmente menos erros que o contrário. Portanto, a real "necessidade", pressão ou custos / riscos gerais de manutenção / correção de bugs para testes extensivos podem não ser altos.

Vamos ao extremo, onde um colega de trabalho da minha equipe sugeriu: temos que tentar testar nosso código de camada de modelo Java EE puro com uma cobertura de 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 casos de uso e fluxos de trabalho da interface do usuário da Web possíveis, porque não queremos arriscar que nenhum caso de uso falhe. Mas temos um orçamento apertado de cerca de 1 milhão de euros, um plano bastante apertado para codificar tudo. Um ambiente de cliente, em que possíveis bugs de aplicativos não representariam um grande perigo para humanos ou empresas. Nosso aplicativo será testado internamente com (alguns) testes de unidade importantes, testes de integração, testes de clientes-chave com planos de teste projetados, uma fase de teste etc. Não desenvolvemos um aplicativo para alguma fábrica nuclear ou produção farmacêutica!

Eu mesmo tento escrever teste, se possível, e desenvolver enquanto teste de unidade meu código. Porém, costumo fazer isso de uma abordagem de cima para baixo (teste de integração) e tento encontrar o ponto positivo, onde cortar a "camada de aplicativo" para os testes importantes (geralmente na camada de modelo). (porque geralmente se trata muito das "camadas")

Além disso, o código do 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 muitos testes desenvolvidos código.

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

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

Andreas Dietrich
fonte