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.
Respostas:
É 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.
fonte
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 ...
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).
fonte
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.
fonte
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.
fonte
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:
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.
fonte
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.
fonte
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.
fonte
(Esta é uma resposta menor. Obrigado @ TimWilliscroft pela dica.)
As falhas são mais fáceis de localizar se:
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.
fonte
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.
fonte
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
fonte
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 :)
fonte