Eu estava assistindo aos webcasts de Rob Connerys no aplicativo MVCStoreFront e percebi que ele estava testando a unidade até as coisas mais mundanas, coisas como:
public Decimal DiscountPrice
{
get
{
return this.Price - this.Discount;
}
}
Teria um teste como:
[TestMethod]
public void Test_DiscountPrice
{
Product p = new Product();
p.Price = 100;
p.Discount = 20;
Assert.IsEqual(p.DiscountPrice,80);
}
Embora eu seja totalmente a favor do teste de unidade, às vezes me pergunto se esta forma de primeiro desenvolvimento do teste é realmente benéfica, por exemplo, em um processo real, você tem 3-4 camadas acima do seu código (Solicitação de Negócios, Documento de Requisitos, Documento de Arquitetura) , em que a regra de negócios definida real (Preço com desconto é Preço - Desconto) pode estar mal definida.
Se for essa a situação, seu teste de unidade não significa nada para você.
Além disso, seu teste de unidade é outro ponto de falha:
[TestMethod]
public void Test_DiscountPrice
{
Product p = new Product();
p.Price = 100;
p.Discount = 20;
Assert.IsEqual(p.DiscountPrice,90);
}
Agora, o teste está falho. Obviamente, em um teste simples, não é grande coisa, mas digamos que estivéssemos testando uma regra de negócios complicada. O que ganhamos aqui?
Avance rapidamente dois anos na vida útil do aplicativo, quando os desenvolvedores de manutenção estão fazendo a manutenção. Agora o negócio muda sua regra e o teste é interrompido novamente, algum desenvolvedor novato corrige o teste incorretamente ... agora temos outro ponto de falha.
Tudo o que vejo são mais possíveis pontos de falha, sem nenhum retorno benéfico real, se o preço com desconto estiver errado, a equipe de teste ainda encontrará o problema, como o teste de unidade economizou algum trabalho?
O que estou perdendo aqui? Por favor, me ensine a amar o TDD, pois estou tendo dificuldade em aceitá-lo como útil até agora. Eu também quero, porque quero continuar progressista, mas simplesmente não faz sentido para mim.
EDIT: Algumas pessoas sempre mencionaram que o teste ajuda a aplicar as especificações. Minha experiência mostra que as especificações também estão erradas, na maioria das vezes, mas talvez eu esteja condenado a trabalhar em uma organização onde as especificações são escritas por pessoas que não deveriam estar escrevendo especificações.
fonte
Respostas:
Em primeiro lugar, o teste é como a segurança - você nunca pode ter 100% de certeza de que a possui, mas cada camada adiciona mais confiança e uma estrutura para corrigir mais facilmente os problemas que permanecem.
Em segundo lugar, você pode dividir os testes em sub-rotinas que podem ser testadas. Quando você tem 20 testes semelhantes, fazer uma sub-rotina (testada) significa que seu teste principal consiste em 20 invocações simples da sub-rotina, o que é muito mais provável de estar correto.
Terceiro, alguns argumentariam que o TDD trata dessa preocupação. Ou seja, se você apenas escrever 20 testes e eles passarem, você não terá total certeza de que eles estão realmente testando alguma coisa. Mas se cada teste que você escreveu inicialmente falhou e depois o corrigiu, então você está muito mais confiante de que está realmente testando seu código. IMHO, esse vai-e-vem leva mais tempo do que vale, mas é um processo que tenta solucionar sua preocupação.
fonte
Um teste errado provavelmente não quebrará seu código de produção. Pelo menos, nada pior do que não ter nenhum teste. Portanto, não é um "ponto de falha": os testes não precisam estar corretos para que o produto realmente funcione. Eles podem ter que estar corretos antes de ser aprovado como funcionando, mas o processo de consertar qualquer teste interrompido não coloca em risco seu código de implementação.
Você pode pensar em testes, mesmo testes triviais como esses, como uma segunda opinião sobre o que o código deve fazer. Uma opinião é o teste, a outra é a implementação. Se eles não concordarem, então você sabe que tem um problema e olha mais de perto.
Também é útil se alguém quiser implementar a mesma interface do zero. Eles não deveriam ter que ler a primeira implementação para saber o que Desconto significa, e os testes atuam como um backup inequívoco de qualquer descrição escrita da interface que você possa ter.
Dito isso, você está trocando tempo. Se houver outros testes que você possa escrever usando o tempo que economiza pulando esses testes triviais, talvez eles sejam mais valiosos. Depende de sua configuração de teste e da natureza do aplicativo, na verdade. Se o desconto for importante para o aplicativo, você irá detectar quaisquer erros neste método no teste funcional de qualquer maneira. Tudo o que o teste de unidade faz é permitir que você os detecte no ponto em que está testando esta unidade, quando a localização do erro será imediatamente óbvia, em vez de esperar até que o aplicativo seja integrado e a localização do erro possa ser menos óbvia.
A propósito, pessoalmente eu não usaria 100 como preço no caso de teste (ou melhor, se o fizesse, adicionaria outro teste com outro preço). O motivo é que, no futuro, alguém pode pensar que o Desconto deveria ser uma porcentagem. Um objetivo de testes triviais como este é garantir que os erros na leitura da especificação sejam corrigidos.
[Sobre a edição: acho que é inevitável que uma especificação incorreta seja um ponto de falha. Se você não sabe o que o aplicativo deve fazer, é provável que não o faça. Mas escrever testes para refletir as especificações não amplia esse problema, simplesmente falha em resolvê-lo. Portanto, você não está adicionando novos pontos de falha, está apenas representando as falhas existentes no código, em vez de na documentação do
waffle.]fonte
O teste de unidade não deve economizar trabalho, mas sim ajudá-lo a encontrar e prevenir bugs. É mais trabalho, mas é o tipo certo de trabalho. É pensar em seu código nos níveis mais baixos de granularidade e escrever casos de teste que provam que ele funciona nas condições esperadas , para um determinado conjunto de entradas. Ele está isolando variáveis para que você possa economizar tempo procurando no lugar certo quando um bug se apresenta. É salvar aquele conjunto de testes para que você possa usá-los novamente quando precisar fazer uma alteração no caminho.
Pessoalmente, acho que a maioria das metodologias não são muitas etapas removidas da engenharia de software de culto de carga , TDD incluído, mas você não precisa aderir ao TDD estrito para colher os benefícios dos testes de unidade. Guarde as partes boas e jogue fora as que rendem pouco benefício.
Finalmente, a resposta à sua pergunta titular " Como você testa um teste de unidade? " É que você não deveria. Cada teste de unidade deve ter morte cerebral simples. Chame um método com uma entrada específica e compare-o com sua saída esperada. Se a especificação de um método mudar, você pode esperar que alguns dos testes de unidade desse método também precisem ser alterados. Esse é um dos motivos pelos quais você faz o teste de unidade em um nível tão baixo de granularidade, de modo que apenas alguns dos testes de unidade precisam ser alterados. Se você descobrir que os testes de muitos métodos diferentes estão mudando para uma mudança em um requisito, talvez você não esteja testando em um nível suficientemente bom de granularidade.
fonte
Os testes de unidade existem para que suas unidades (métodos) façam o que você espera. Escrever o teste primeiro força você a pensar sobre o que você espera antes de escrever o código. Pensar antes de fazer é sempre uma boa ideia.
Os testes de unidade devem refletir as regras de negócios. É verdade que pode haver erros no código, mas escrever o teste primeiro permite que você o faça da perspectiva da regra de negócios antes que qualquer código seja escrito. Escrever o teste depois, eu acho, é mais provável de levar ao erro que você descreve, porque você sabe como o código o implementa e fica tentado apenas a se certificar de que a implementação está correta - não que a intenção esteja correta.
Além disso, os testes de unidade são apenas uma forma - e a mais baixa - de testes que você deve escrever. Testes de integração e testes de aceitação também devem ser escritos, estes últimos pelo cliente, se possível, para garantir que o sistema opere da maneira esperada. Se você encontrar erros durante este teste, volte e escreva testes de unidade (que falham) para testar a mudança na funcionalidade para fazê-la funcionar corretamente e, em seguida, altere seu código para fazer o teste passar. Agora você tem testes de regressão que capturam suas correções de bugs.
[EDITAR]
Outra coisa que descobri ao fazer TDD. Quase força um bom design por padrão. Isso ocorre porque projetos altamente acoplados são quase impossíveis de testar de forma isolada. Não leva muito tempo usando TDD para descobrir que usar interfaces, inversão de controle e injeção de dependência - todos os padrões que irão melhorar seu design e reduzir o acoplamento - são realmente importantes para código testável.
fonte
Como se testa um teste ? O teste de mutação é uma técnica valiosa que usei pessoalmente com resultados surpreendentemente bons. Leia o artigo vinculado para mais detalhes e links para ainda mais referências acadêmicas, mas em geral ele "testa seus testes" modificando seu código-fonte (alterando "x + = 1" para "x - = 1", por exemplo) e então executar novamente seus testes, garantindo que pelo menos um teste falhe. Todas as mutações que não causam falhas no teste são sinalizadas para investigação posterior.
Você ficaria surpreso em saber como pode ter 100% de cobertura de linha e ramificação com um conjunto de testes que parecem abrangentes e, ainda assim, pode alterar fundamentalmente ou até mesmo comentar uma linha em sua fonte sem nenhum dos testes reclamar. Freqüentemente, isso se resume a não testar com as entradas certas para cobrir todos os casos limites, às vezes é mais sutil, mas em todos os casos fiquei impressionado com o quanto saiu disso.
fonte
Ao aplicar o Test-Driven Development (TDD), começa-se com um teste reprovado . Esta etapa, que pode parecer desnecessária, na verdade existe para verificar se o teste de unidade está testando algo. Na verdade, se o teste nunca falhar, ele não traz nenhum valor e, pior, leva a uma confiança errada, pois você confiará em um resultado positivo que não está provando nada.
Ao seguir este processo estritamente, todas as '' unidades '' são protegidas pela rede de segurança que os testes de unidade estão criando, mesmo os mais mundanos.
Não há motivo para o teste evoluir nessa direção - ou estou perdendo algo em seu raciocínio. Quando o preço é 100 e o desconto 20, o preço com desconto é 80. Isso é como um invariante.
Agora imagine que seu software precise suportar outro tipo de desconto baseado em porcentagem, talvez dependendo do volume comprado, seu método Product :: DiscountPrice () pode se tornar mais complicado. E é possível que a introdução dessas mudanças viole a regra de desconto simples que tínhamos inicialmente. Então você verá o valor deste teste que detectará a regressão imediatamente.
Vermelho - Verde - Refatorar - para lembrar a essência do processo de TDD.
Vermelho refere-se à barra vermelha JUnit quando um teste falha.
Verde é a cor da barra de progresso JUnit quando todos os testes passam.
Refatore na condição verde: remova qualquer duplicação, melhore a legibilidade.
Agora, para abordar seu ponto sobre as "3-4 camadas acima do código", isso é verdade em um processo tradicional (tipo cascata), não quando o processo de desenvolvimento é ágil. E ágil é o mundo de onde vem o TDD; TDD é a base da eXtreme Programming .
O Agile trata de comunicação direta, e não de documentos de requisitos jogados no lixo.
fonte
Testes pequenos e triviais como este podem ser o "canário na mina de carvão" para sua base de código, alertando sobre o perigo antes que seja tarde demais. É útil manter os testes triviais porque ajudam a obter as interações certas.
Por exemplo, pense em um teste trivial colocado em prática para investigar como usar uma API com a qual você não está familiarizado. Se esse teste for relevante para o que você está fazendo no código que usa a API "de verdade", é útil mantê-lo por perto. Quando a API lança uma nova versão e você precisa fazer upgrade. Agora você tem suas suposições sobre como espera que a API se comporte, gravadas em um formato executável que pode ser usado para capturar regressões.
Se você já codifica há anos sem escrever testes, pode não ser imediatamente óbvio para você que existe algum valor. Mas se você acha que a melhor maneira de trabalhar é "lançar mais cedo, lançar com frequência" ou "ágil" no sentido de que deseja implementar rapidamente / continuamente, então seu teste definitivamente significa algo. A única maneira de fazer isso é legitimar todas as alterações feitas no código com um teste. Não importa o quão pequeno seja o teste, uma vez que você tenha um conjunto de testes verde, você está teoricamente bem para implantar. Veja também "produção contínua" e "beta perpétuo".
Você não precisa ser o "primeiro a testar" para ter essa mentalidade, mas geralmente essa é a maneira mais eficiente de chegar lá. Quando você faz TDD, você se trava em um pequeno ciclo Red Green Refactor de dois a três minutos. Em nenhum momento você não será capaz de parar e sair e ter uma bagunça completa em suas mãos que levará uma hora para depurar e consertar.
Um teste bem-sucedido é aquele que demonstra uma falha no sistema. Um teste com falha irá alertá-lo sobre um erro na lógica do teste ou na lógica do seu sistema. O objetivo de seus testes é quebrar seu código ou provar que um cenário funciona.
Se você está escrevendo testes após o código, corre o risco de escrever um teste que é "ruim" porque, para ver se seu teste realmente funciona, você precisa vê-lo quebrado e funcionando. Quando você está escrevendo testes após o código, isso significa que você deve "desencadear a armadilha" e introduzir um bug no código para ver o teste falhar. A maioria dos desenvolvedores não está apenas preocupada com isso, mas argumentaria que é uma perda de tempo.
Definitivamente, há uma vantagem em fazer as coisas dessa maneira. Michael Feathers define "código legado" como "código não testado". Ao adotar essa abordagem, você legitima todas as alterações feitas em sua base de código. É mais rigoroso do que não usar testes, mas quando se trata de manter uma grande base de código, ele se paga.
Por falar em penas, existem dois excelentes recursos que você deve verificar a respeito disso:
Ambos explicam como trabalhar esses tipos de práticas e disciplinas em projetos que não são "greenfield". Eles fornecem técnicas para escrever testes em torno de componentes fortemente acoplados, dependências com fio e coisas sobre as quais você não tem necessariamente controle. É tudo uma questão de encontrar "costuras" e testar em torno delas.
Hábitos como esses são como um investimento. Os retornos não são imediatos; eles se acumulam com o tempo. A alternativa para não testar é essencialmente assumir a dívida de não ser capaz de capturar regressões, introduzir código sem medo de erros de integração ou conduzir decisões de design. A beleza é que você legitima todas as alterações introduzidas em sua base de código.
Eu vejo isso como uma responsabilidade profissional. É um ideal pelo qual se empenhar. Mas é muito difícil de seguir e tedioso. Se você se preocupa com isso e acha que não deve produzir código que não seja testado, poderá encontrar a força de vontade para aprender bons hábitos de teste. Uma coisa que eu faço muito agora (assim como outros) é um tempo de uma hora para escrever código sem nenhum teste e, em seguida, ter a disciplina de jogá-lo fora. Isso pode parecer um desperdício, mas não é realmente. Não é como se o exercício custasse materiais físicos à empresa. Isso me ajudou a entender o problema e como escrever código de forma que seja de melhor qualidade e testável.
Em última análise, meu conselho seria que, se você realmente não deseja ser bom nisso, não o faça de forma alguma. Testes ruins que não são mantidos, não funcionam bem etc. podem ser piores do que não ter nenhum teste. É difícil aprender sozinho, e você provavelmente não vai adorar, mas será quase impossível aprender se você não tiver o desejo de fazê-lo ou não puder ver o valor suficiente para garante o investimento de tempo.
O teclado do desenvolvedor é onde a borracha encontra a estrada. Se a especificação estiver errada e você não levantar a bandeira sobre ela, é altamente provável que você seja culpado por isso. Ou pelo menos seu código vai. A disciplina e o rigor envolvidos no teste são difíceis de aderir. Não é nada fácil. É preciso prática, muito aprendizado e muitos erros. Mas eventualmente vale a pena. Em um projeto acelerado e em constante mudança, é a única maneira de você dormir à noite, não importa se isso atrapalhe.
Outra coisa a se pensar aqui é que as técnicas que são fundamentalmente iguais aos testes já provaram funcionar no passado: "sala limpa" e "projeto por contrato" tendem a produzir os mesmos tipos de construções de "meta" código que os testes fazem e os aplicam em diferentes pontos. Nenhuma dessas técnicas é bala de prata, e o rigor vai custar caro no que se refere aos recursos que você pode oferecer em termos de tempo de lançamento no mercado. Mas não é disso que se trata. É sobre ser capaz de manter o que você entrega. E isso é muito importante para a maioria dos projetos.
fonte
O teste de unidade funciona de maneira muito semelhante à contabilidade por partidas dobradas. Você afirma a mesma coisa (regra de negócios) de duas maneiras bem diferentes (como regras programadas em seu código de produção e como exemplos simples e representativos em seus testes). É muito improvável que você cometa o mesmo erro em ambos, então, se ambos concordarem, é bem improvável que você tenha errado.
Como o teste vai valer a pena? Na minha experiência de pelo menos quatro maneiras, pelo menos ao fazer o desenvolvimento orientado a testes:
fonte
A maioria dos testes de unidade, suposições de teste. Nesse caso, o preço com desconto deve ser o preço menos o desconto. Se suas suposições estiverem erradas, aposto que seu código também está errado. E se você cometer um erro bobo, o teste irá falhar e você irá corrigi-lo.
Se as regras mudarem, o teste falhará e isso é uma coisa boa. Então você tem que mudar o teste também neste caso.
Como regra geral, se um teste falhar imediatamente (e você não usar o design teste primeiro), o teste ou o código estão errados (ou ambos, se você estiver tendo um dia ruim). Você usa o bom senso (e possivelmente as especificações) para corrigir o código incorreto e executar o teste novamente.
Como disse Jason, testar é segurança. E sim, às vezes eles introduzem trabalho extra por causa de testes defeituosos. Mas na maioria das vezes, eles economizam muito tempo. (E você tem a oportunidade perfeita de punir o cara que quebra o teste (estamos falando de galinha de borracha)).
fonte
Teste tudo o que puder. Mesmo erros triviais, como esquecer de converter metros em pés, podem ter efeitos colaterais muito caros. Escreva um teste, escreva o código para ele verificar, faça-o passar, siga em frente. Quem sabe em algum momento no futuro, alguém pode alterar o código de desconto. Um teste pode detectar o problema.
fonte
Vejo os testes de unidade e o código de produção como tendo uma relação simbiótica. Simplificando: um testa o outro. E ambos testam o desenvolvedor.
fonte
Lembre-se de que o custo de correção de defeitos aumenta (exponencialmente) à medida que os defeitos sobrevivem ao longo do ciclo de desenvolvimento. Sim, a equipe de teste pode detectar o defeito, mas (geralmente) vai dar mais trabalho para isolar e corrigir o defeito daquele ponto do que se um teste de unidade tivesse falhado, e será mais fácil introduzir outros defeitos enquanto corrige-o se você não tem testes de unidade para executar.
Isso geralmente é mais fácil de ver com algo mais do que um exemplo trivial ... e com exemplos triviais, bem, se você de alguma forma bagunçar o teste de unidade, a pessoa que o está revisando irá detectar o erro no teste ou o erro no código, ou ambos. (Eles estão sendo revisados, certo?) Como tvanfosson aponta , o teste de unidade é apenas uma parte de um plano de SQA.
Em certo sentido, os testes de unidade são seguros. Eles não garantem que você detectará todos os defeitos e, às vezes, pode parecer que você está gastando muitos recursos com eles, mas quando eles detectam defeitos que você pode corrigir, você gastará muito menos do que se você não tivesse feito nenhum teste e tivesse que corrigir todos os defeitos posteriores.
fonte
Eu vejo seu ponto, mas é claramente exagerado.
Seu argumento é basicamente: os testes introduzem o fracasso. Portanto, os testes são ruins / perda de tempo.
Embora isso possa ser verdade em alguns casos, dificilmente é a maioria.
O TDD assume: Mais testes = menos falhas.
Os testes têm mais probabilidade de detectar pontos de falha do que apresentá-los.
fonte
Ainda mais automação pode ajudar aqui! Sim, escrever testes de unidade pode ser muito trabalhoso, então use algumas ferramentas para ajudá-lo. Dê uma olhada em algo como Pex, da Microsoft, se você estiver usando .Net Ele criará automaticamente suítes de testes de unidade para você examinando seu código. Ele virá com testes que darão uma boa cobertura, tentando cobrir todos os caminhos através do seu código.
Claro, só de olhar para o seu código ele não pode saber o que você estava realmente tentando fazer, portanto, não sabe se está correto ou não. Porém, ele gerará casos de teste interessantes para você, e você poderá examiná-los e ver se ele está se comportando conforme o esperado.
Se você for além e escrever testes de unidade parametrizados (você pode pensar neles como contratos, na verdade), isso gerará casos de teste específicos a partir deles, e desta vez ele pode saber se algo está errado, porque suas afirmações em seus testes irão falhar.
fonte
Pensei um pouco em uma boa maneira de responder a essa pergunta e gostaria de traçar um paralelo com o método científico. IMO, você poderia reformular esta pergunta: "Como você faz um experimento?"
Os experimentos verificam suposições empíricas (hipóteses) sobre o universo físico. Os testes de unidade testarão suposições sobre o estado ou comportamento do código que eles chamam. Podemos falar sobre a validade de um experimento, mas isso porque sabemos, por meio de vários outros experimentos, que algo não se encaixa. Não tem validade convergente e evidência empírica . Não projetamos um novo experimento para testar ou verificar a validade de um experimento , mas podemos projetar um experimento completamente novo .
Assim, como os experimentos , não descrevemos a validade de um teste de unidade com base em sua aprovação ou não em um teste de unidade. Junto com outros testes de unidade, ele descreve as suposições que fazemos sobre o sistema que está testando. Além disso, como os experimentos, tentamos remover o máximo de complexidade possível do que estamos testando. "O mais simples possível, mas não mais simples."
Ao contrário dos experimentos , temos um truque na manga para verificar se nossos testes são válidos, além da validade convergente. Podemos habilmente introduzir um bug que sabemos que deve ser detectado pelo teste e ver se o teste realmente falha. (Se pudéssemos fazer isso no mundo real, dependeríamos muito menos dessa coisa de validade convergente!) Uma maneira mais eficiente de fazer isso é assistir seu teste falhar antes de implementá-lo (a etapa vermelha em Vermelho, Verde, Refatorar )
fonte
Você precisa usar o paradigma correto ao escrever testes.
Você nem sempre pode ter certeza, mas eles melhoram os testes gerais.
fonte
Mesmo que você não teste seu código, ele certamente será testado em produção por seus usuários. Os usuários são muito criativos ao tentar travar seu software e encontrar até mesmo erros não críticos.
Corrigir bugs na produção é muito mais caro do que resolver problemas na fase de desenvolvimento. Como efeito colateral, você perderá receita devido ao êxodo de clientes. Você pode contar com 11 clientes perdidos ou não conquistados para 1 cliente irritado.
fonte