Fui encarregado de escrever testes de unidade para um aplicativo existente. Depois de terminar meu primeiro arquivo, tenho 717 linhas de código de teste para 419 linhas de código original.
Essa proporção se tornará incontrolável à medida que aumentamos nossa cobertura de código?
Minha compreensão do teste de unidade era testar cada método da classe para garantir que todos os métodos funcionassem conforme o esperado. No entanto, na solicitação de recebimento, meu líder técnico observou que eu deveria me concentrar em testes de nível superior. Ele sugeriu testar 4-5 casos de uso mais usados com a classe em questão, em vez de testar exaustivamente cada função.
Confio no comentário do meu líder técnico. Ele tem mais experiência do que eu e tem melhores instintos quando se trata de projetar software. Mas como uma equipe de várias pessoas escreve testes para um padrão tão ambíguo; ou seja, como conheço meus colegas e compartilho a mesma idéia para os "casos de uso mais comuns"?
Para mim, 100% de cobertura de teste de unidade é uma meta elevada, mas mesmo que atingíssemos apenas 50%, saberíamos que 100% desses 50% estavam cobertos. Caso contrário, escrever testes para parte de cada arquivo deixa muito espaço para trapacear.
fonte
Respostas:
Sim, com 100% de cobertura, você escreverá alguns testes desnecessários. Infelizmente, a única maneira confiável de determinar quais testes você não precisa é escrever todos eles, depois aguarde 10 anos ou mais para ver quais nunca falharam.
Manter muitos testes geralmente não é problemático. Muitas equipes têm testes automatizados de integração e sistema com 100% de cobertura de teste de unidade.
No entanto, você não está em uma fase de manutenção de teste, está tentando recuperar o atraso. É muito melhor ter 100% de suas aulas com 50% de cobertura de teste do que 50% de suas aulas com 100% de cobertura de teste, e seu líder parece estar tentando fazer com que você aloque seu tempo adequadamente. Depois de ter essa linha de base, a próxima etapa geralmente é pressionar 100% nos arquivos que serão alterados daqui para frente.
fonte
Se você trabalhou em grandes bases de código criadas usando o Test Driven Development, já saberia que pode haver muitos testes de unidade. Em alguns casos, a maior parte do esforço de desenvolvimento consiste em atualizar testes de baixa qualidade que seriam melhor implementados como verificações invariáveis, pré-condição e pós-condição nas classes relevantes, em tempo de execução (ou seja, testar como efeito colateral de um teste de nível superior) )
Outro problema é a criação de projetos de baixa qualidade, usando técnicas de projeto dirigidas por cultos de carga, que resultam em uma proliferação de coisas a serem testadas (mais classes, interfaces, etc.). Nesse caso, a carga pode parecer estar atualizando o código de teste, mas o verdadeiro problema é o design de baixa qualidade.
fonte
Respostas às suas perguntas
Claro ... Você pode, por exemplo, ter vários testes que parecem diferentes à primeira vista, mas realmente testam a mesma coisa (depende logicamente das mesmas linhas de código de aplicativo "interessante" em teste).
Ou você pode testar as partes internas do seu código que nunca surgem externamente (ou seja, não fazem parte de nenhum tipo de contrato de interface), onde se pode discutir se isso faz sentido. Por exemplo, o texto exato das mensagens de log internas ou o que for.
Isso me parece bastante normal. Seus testes gastam muitas linhas de código na instalação e desmontagem, além dos testes reais. A proporção pode melhorar ou não. Eu próprio sou bastante pesado em testes e frequentemente investo mais tempo e local nos testes do que o código real.
A proporção não leva em consideração tanto. Existem outras qualidades de testes que tendem a torná-los incontroláveis. Se você precisar refatorar regularmente vários testes ao fazer alterações bastante simples no seu código, dê uma boa olhada nos motivos. E essas não são quantas linhas você tem, mas como você aborda a codificação dos testes.
Isso é correto para testes "unitários" no sentido estrito. Aqui, "unidade" é algo como um método ou uma classe. O objetivo do teste "unitário" é testar apenas uma unidade específica de código, não o sistema inteiro. Idealmente, você removeria todo o resto do sistema (usando dobros ou outros enfeites).
Então você caiu na armadilha de supor que as pessoas realmente significavam testes de unidade quando diziam testes de unidade. Eu conheci muitos programadores que dizem "teste de unidade", mas significam algo bem diferente.
Claro, concentrar-se apenas nos 80% dos códigos importantes também reduz a carga ... Compreendo que você pense muito em seu chefe, mas isso não me parece a melhor opção.
Não sei o que é "cobertura de teste de unidade". Suponho que você queira dizer "cobertura de código", ou seja, depois de executar o conjunto de testes, todas as linhas de código (= 100%) foram executadas pelo menos uma vez.
Essa é uma boa métrica de estimativa, mas de longe não é o melhor padrão para o qual alguém poderia atirar. Apenas executar linhas de código não é a imagem toda; isso não leva em conta caminhos diferentes por meio de ramificações aninhadas e complicadas, por exemplo. É mais uma métrica que aponta o dedo para partes de código que são testadas muito pouco (obviamente, se uma classe tem 10% ou 5% de cobertura de código, algo está errado); por outro lado, uma cobertura de 100% não informa se você testou o suficiente ou se testou corretamente.
Teste de integração
Irrita-me substancialmente quando as pessoas estão constantemente falando sobre testes de unidade hoje, por padrão. Na minha opinião (e experiência), o teste de unidade é ótimo para bibliotecas / APIs; em áreas mais voltadas para negócios (onde falamos de casos de uso, como na pergunta em questão), elas não são necessariamente a melhor opção.
Para o código geral do aplicativo e para as empresas comuns (onde é importante ganhar dinheiro, cumprir prazos e satisfazer a satisfação do cliente, é importante evitar erros que estão diretamente na cara do usuário ou que podem levar a desastres reais - não estamos falando lançamentos de foguetes da NASA aqui), testes de integração ou de recursos são muito mais úteis.
Aqueles andam de mãos dadas com o Desenvolvimento Orientado ao Comportamento ou ao Desenvolvimento Orientado a Recursos; aqueles não funcionam com testes de unidade (estritos), por definição.
Para mantê-lo curto (ish), um teste de integração / recurso exercita toda a pilha de aplicativos. Em um aplicativo baseado na Web, ele funcionaria como um navegador clicando no aplicativo (e não, obviamente, não precisa ser tão simplista, existem estruturas muito poderosas para fazer isso - confira http: // pepino. io por exemplo).
Ah, para responder às suas últimas perguntas: você faz com que toda a sua equipe tenha uma alta cobertura de teste, certificando-se de que um novo recurso seja programado apenas após a implementação e falha do teste. E sim, isso significa todos os recursos. Isso garante uma cobertura de recurso 100% (positiva). Por definição, garante que um recurso do seu aplicativo nunca "desapareça". Ele não garante uma cobertura de código de 100% (por exemplo, a menos que você programe ativamente recursos negativos, não estará exercitando seu tratamento de erros / tratamento de exceções).
Ele não garante um aplicativo sem erros; é claro que você deseja escrever testes de recursos para situações de bugs óbvias ou muito perigosas, entrada incorreta do usuário, hackers (por exemplo, gerenciamento de sessões, segurança e outros) etc .; mas mesmo a programação dos testes positivos tem um tremendo benefício e é bastante viável com estruturas modernas e poderosas.
Os testes de recursos / integração obviamente têm sua própria lata de worms (por exemplo, desempenho; teste redundante de estruturas de terceiros; como você geralmente não usa duplos, eles também tendem a ser mais difíceis de escrever, na minha experiência ...), mas eu ' d use um aplicativo 100% testado por recurso positivo em vez de um aplicativo 100% testado por unidade de cobertura de código (não biblioteca!) a qualquer dia.
fonte
Sim, é possível ter muitos testes de unidade. Se você possui 100% de cobertura com testes de unidade e sem testes de integração, por exemplo, você tem um problema claro.
Alguns cenários:
Você superdimensiona seus testes para uma implementação específica. Então você deve jogar fora os testes de unidade quando refatorar, para não dizer quando alterar a implementação (um ponto problemático muito frequente ao executar otimizações de desempenho).
Um bom equilíbrio entre testes de unidade e testes de integração reduz esse problema sem perder uma cobertura significativa.
Você pode ter uma cobertura razoável para cada confirmação com 20% dos testes que você possui, deixando os 80% restantes para integração ou pelo menos passados testes separados; os principais efeitos negativos que você vê neste cenário são mudanças lentas, pois você precisa esperar muito tempo para que os testes sejam executados.
Você modifica muito o código para permitir que você o teste; por exemplo, eu vi muitos abusos da IoC em componentes que nunca precisam ser modificados ou, pelo menos, é caro e de baixa prioridade generalizá-los, mas as pessoas investem muito tempo em generalizá-las e refatorá-las para permitir testes de unidade .
Concordo particularmente com a sugestão de obter 50% de cobertura em 100% dos arquivos, em vez de 100% de cobertura em 50% dos arquivos; concentre seus esforços iniciais nos casos positivos mais comuns e nos casos negativos mais perigosos, não invista muito no tratamento de erros e caminhos incomuns, não porque eles não sejam importantes, mas porque você tem um tempo limitado e um universo infinito de testes, então você precisa priorizar em qualquer caso.
fonte
Lembre-se de que cada teste tem um custo e um benefício. Os inconvenientes incluem:
Se os custos superam os benefícios, é melhor um teste não ser escrito. Por exemplo, se a funcionalidade é difícil de testar, a API muda com frequência, a correção é relativamente sem importância e a chance de o teste encontrar um defeito é baixa, é melhor você não escrevê-lo.
Quanto à sua proporção específica de testes para código, se o código for suficientemente denso da lógica, essa proporção poderá ser garantida. No entanto, provavelmente não vale a pena manter uma proporção tão alta em um aplicativo típico.
fonte
Sim, existem muitos testes de unidade.
Enquanto o teste é bom, todos os testes de unidade são:
Uma carga potencial de manutenção fortemente acoplada à API
Tempo que poderia ser gasto em outra coisa
É aconselhável procurar 100% de cobertura de código, mas isso longe de significar um conjunto de testes, cada um dos quais fornece independentemente 100% de cobertura de código em algum ponto de entrada especificado (função / método / chamada etc.).
Embora dado o quão difícil seja possível obter uma boa cobertura e eliminar os erros, é provável que exista algo como "os testes unitários errados" e "muitos testes unitários".
Pragmática para a maioria dos códigos indica:
Certifique-se de ter 100% de cobertura dos pontos de entrada (tudo é testado de alguma forma) e procure estar perto de 100% de cobertura de código dos caminhos de 'não erros'.
Teste quaisquer valores ou tamanhos mínimos / máximos relevantes
Teste qualquer coisa que você pense ser um caso especial engraçado, particularmente valores "ímpares".
Quando você encontrar um bug, adicione um teste de unidade que o tenha revelado e pense se algum caso semelhante deve ser adicionado.
Para algoritmos mais complexos, considere também:
Por exemplo, verifique um algoritmo de classificação com alguma entrada aleatória e a validação dos dados é classificada no final, varrendo-o.
Eu diria que seu líder técnico está propondo testes de 'mínima bunda'. Estou oferecendo 'testes de qualidade de maior valor' e existe um espectro entre eles.
Talvez o seu veterano saiba que o componente que você está construindo será incorporado em uma peça maior e a unidade será testada mais detalhadamente quando integrada.
A principal lição é adicionar testes quando forem encontrados erros. O que me leva a minha melhor lição sobre o desenvolvimento de testes de unidade:
Concentre-se nas unidades e não nas sub-unidades. Se você estiver construindo uma unidade a partir de subunidades, escreva testes muito básicos para as subunidades até que sejam plausíveis e obtenha uma melhor cobertura testando as subunidades por meio de suas unidades de controle.
Portanto, se você estiver escrevendo um compilador e precisar escrever uma tabela de símbolos (digamos). Coloque a tabela de símbolos em funcionamento com um teste básico e trabalhe (digamos) no analisador de declaração que preenche a tabela. Adicione mais testes à unidade 'autônoma' da tabela de símbolos se encontrar erros nela. Caso contrário, aumente a cobertura por testes de unidade no analisador de declaração e posteriormente em todo o compilador.
Isso obtém o melhor retorno possível (um teste do todo está testando vários componentes) e deixa mais capacidade de reprojetar e aperfeiçoar, porque apenas a interface 'externa' é usada em testes que tendem a ser mais estáveis.
Juntamente com as pré-condições de teste do código de depuração, as pós-condições, incluindo invariantes em todos os níveis, você obtém a cobertura máxima do teste com a implementação mínima do teste.
fonte
Em primeiro lugar, não é necessariamente um problema ter mais linhas de teste do que código de produção. O código de teste é (ou deveria ser) linear e fácil de compreender - sua complexidade necessária é muito, muito baixa, independentemente de o código de produção ser ou não. Se a complexidade dos testes começar a se aproximar da do código de produção, é provável que você tenha um problema.
Sim, é possível ter muitos testes de unidade - um simples experimento mental mostra que você pode continuar adicionando testes que não fornecem valor adicional e que todos esses testes adicionados podem inibir pelo menos algumas refatorações.
O conselho para testar apenas os casos mais comuns é falho, na minha opinião. Eles podem atuar como testes de fumaça para economizar tempo de teste do sistema, mas os testes realmente valiosos capturam casos difíceis de exercitar em todo o sistema. Por exemplo, a injeção de erro controlada de falhas de alocação de memória pode ser usada para exercer caminhos de recuperação que, de outra forma, poderiam ser de qualidade completamente desconhecida. Ou passe zero como um valor que você sabe que será usado como um divisor (ou um número negativo com raiz quadrada) e verifique se você não recebe uma exceção não tratada.
Os próximos testes mais valiosos são aqueles que exercitam limites ou pontos extremos. Por exemplo, uma função que aceita meses (com base em 1) do ano deve ser testada com 0, 1, 12 e 13, para que você saiba que as transições válido-inválidas estão no lugar certo. É um teste excessivo também usar 2..11 para esses testes.
Você está em uma posição difícil, pois precisa escrever testes para o código existente. É mais fácil identificar os casos extremos à medida que você está escrevendo (ou prestes a escrever) o código.
fonte
Esse entendimento está errado.
Os testes de unidade verificam o comportamento da unidade em teste .
Nesse sentido, uma unidade não é necessariamente "um método em uma classe". Gosto da definição de uma unidade de Roy Osherove em The Art of Unit Testing :
Com base nisso, um teste de unidade deve verificar todo comportamento desejado do seu código. Onde o "desejo" é mais ou menos retirado dos requisitos.
Ele está certo, mas de uma maneira diferente do que ele pensa.
Pela sua pergunta, entendo que você é o "testador dedicado" nesse projeto.
O grande mal-entendido é que ele espera que você escreva testes de unidade (em contraste com "teste usando uma estrutura de teste de unidade"). Escrever testes ynit é de responsabilidade dos desenvolvedores , não dos testadores (em um mundo ideal, eu sei ...). Por outro lado, você marcou essa pergunta com TDD, o que implica exatamente isso.
Seu trabalho como testador é escrever (ou executar manualmente) módulos e / ou testes de aplicativos. E esse tipo de teste deve verificar principalmente se todas as unidades funcionam juntas sem problemas. Isso significa que você deve selecionar seus casos de teste para que cada unidade seja executada pelo menos uma vez . E essa verificação é que é executado. O resultado real é menos importante, pois está sujeito a alterações com requisitos futuros.
Para enfatizar a analogia do automóvel de despejo mais uma vez: Quantos testes são feitos com um carro no final da linha de montagem? Exatamente um: ele deve dirigir até o estacionamento sozinho ...
O ponto aqui é:
Precisamos estar cientes dessa diferença entre "testes de unidade" e "teste automatizado usando uma estrutura de teste de unidade".
Os testes de unidade são uma rede de segurança. Eles dão a você a confiança de refatorar seu código para reduzir a dívida técnica ou adicionar um novo comportamento sem medo de quebrar o comportamento já implementado.
Você não precisa de 100% de cobertura de código.
Mas você precisa de 100% de cobertura comportamental. (Sim, a cobertura do código e a cobertura do comportamento se correlacionam de alguma forma, mas não são idênticas por isso.)
Se você tiver menos de 100% de cobertura de comportamento, uma execução bem-sucedida do seu conjunto de testes não significa nada, pois você pode ter alterado um pouco do comportamento não testado. E você será notado pelo seu cliente no dia seguinte ao lançamento da sua versão online ...
Conclusão
Poucos testes são melhores que nenhum teste. Sem dúvida!
Mas não existe tal coisa como ter muitos testes de unidade.
Isso ocorre porque cada teste de unidade verifica uma única expectativa sobre o comportamento dos códigos . E você não pode escrever mais testes de unidade do que as expectativas em seu código. E um furo no seu cinto de segurança é a chance de uma alteração indesejada prejudicar o sistema de produção.
fonte
Absolutamente sim. Eu costumava ser um SDET para uma grande empresa de software. Nossa pequena equipe teve que manter o código de teste que costumava ser tratado por uma equipe muito maior. Além disso, nosso produto possuía algumas dependências que estavam constantemente introduzindo alterações significativas, o que significa manutenção de teste constante para nós. Como não tínhamos a opção de aumentar o tamanho da equipe, tivemos que jogar fora milhares dos testes menos valiosos quando eles falharam. Caso contrário, nunca conseguiríamos acompanhar os defeitos.
Antes de descartar isso como um mero problema de gerenciamento, considere que muitos projetos no mundo real sofrem com redução de pessoal à medida que se aproximam do status de legado. Às vezes, isso começa a acontecer logo após o primeiro lançamento.
fonte
Ter mais linhas de código de teste que código de produto não é necessariamente um problema, supondo que você esteja refatorando seu código de teste para eliminar a copiar e colar.
O problema é ter testes que são espelhos de sua implementação, sem significado comercial - por exemplo, testes carregados com zombarias e stubs e afirmar apenas que um método chama outro método.
Uma ótima citação no artigo "por que a maioria dos testes de unidade é desperdício" é que os testes de unidade devem ter um "oráculo amplo, formal e independente de correção, e ... valor comercial atribuível"
fonte
Uma coisa que não vi mencionada é que seus testes precisam ser rápidos e fáceis para qualquer desenvolvedor executar a qualquer momento.
Você não precisa fazer check-in no controle de origem e aguardar uma hora ou mais (dependendo do tamanho da sua base de código) antes que os testes sejam concluídos para verificar se sua alteração quebrou algo - você pode fazer isso em sua própria máquina antes de fazer o check-in no controle de origem (ou pelo menos antes de enviar suas alterações). Idealmente, você poderá executar seus testes com um único script ou botão.
E quando você executa esses testes localmente, deseja que eles sejam executados rapidamente - na ordem de segundos. Mais devagar, e você será tentado a não executá-los o suficiente ou de maneira alguma.
Portanto, ter tantos testes que executá-los todos leva minutos ou ter alguns testes excessivamente complexos pode ser um problema.
fonte