Você tem uma classe X e escreve alguns testes de unidade que verificam o comportamento X1. Há também a classe A, que toma X como uma dependência.
Ao escrever testes de unidade para A, você zomba de X. Em outras palavras, durante o teste de unidade A, você define (postula) que o comportamento da zombaria de X é X1. O tempo passa, as pessoas usam seu sistema, precisam mudar, X evolui: você modifica X para mostrar o comportamento X2. Obviamente, os testes de unidade do X falharão e você precisará adaptá-los.
Mas o que com A? Os testes de unidade para A não falharão quando o comportamento de X for modificado (devido à zombaria de X). Como detectar que o resultado de A será diferente quando executado com o X "real" (modificado)?
Estou esperando respostas ao longo da linha de: "Esse não é o objetivo do teste de unidade", mas que valor tem então o teste de unidade? Isso realmente apenas lhe diz que, quando todos os testes passam, você não introduziu uma mudança de quebra? E quando o comportamento de alguma classe muda (de boa ou má vontade), como você pode detectar (de preferência de forma automatizada) todas as consequências? Não devemos nos concentrar mais nos testes de integração?
fonte
X1
está dizendo queX
implementa interfaceX1
. Se você alterar a interfaceX1
paraX2
a simulação que você usou nos outros testes não deverá mais compilar, portanto, você será forçado a corrigir esses testes também. Mudanças no comportamento da classe não devem importar. De fato, sua classeA
não deve depender dos detalhes da implementação (que é o que você mudaria nesse caso). Portanto, os testes de unidadeA
ainda estão corretos e eles informam queA
funciona, dada a implementação ideal da interface.Respostas:
Você? Não, a menos que seja absolutamente necessário. Eu tenho que se:
X
é lento ouX
tem efeitos colateraisSe nenhuma dessas opções se aplicar, meus testes de unidade também
A
serão testadosX
. Fazer qualquer outra coisa seria levar os testes de isolamento a um extremo ilógico.Se você tem partes do seu código usando simulações de outras partes, então eu concordo: qual é o sentido desses testes de unidade? Então não faça isso. Deixe esses testes usarem as dependências reais, pois eles formam testes muito mais valiosos dessa maneira.
E se algumas pessoas ficam chateadas com você chamando esses testes de "testes unitários", basta chamá-los de "testes automatizados" e continuar escrevendo bons testes automatizados.
fonte
Você precisa dos dois. Testes de unidade para verificar o comportamento de cada uma de suas unidades e alguns testes de integração para garantir que eles estejam conectados corretamente. O problema de depender apenas de testes de integração é a explosão combinatória resultante de interações entre todas as suas unidades.
Digamos que você tenha a classe A, que exige 10 testes de unidade para cobrir totalmente todos os caminhos. Então você tem outra classe B, que também exige 10 testes de unidade para cobrir todos os caminhos que o código pode percorrer. Agora, digamos que em seu aplicativo, você precisa alimentar a saída de A em B. Agora, seu código pode seguir 100 caminhos diferentes, da entrada de A à saída de B.
Com os testes de unidade, você só precisa de 20 testes de unidade + 1 teste de integração para cobrir completamente todos os casos.
Com os testes de integração, você precisará de 100 testes para cobrir todos os caminhos de código.
Aqui está um vídeo muito bom sobre as desvantagens de confiar apenas nos testes de integração. JB Rainsberger Integrated Tests Are A Scam HD
fonte
Uau, espere um momento. As implicações dos testes para a falha do X são importantes demais para serem ignoradas dessa maneira.
Se alterar a implementação do X de X1 para X2 interromper os testes de unidade do X, isso indica que você fez uma alteração incompatível com o contrato X.
X2 não é um X, no sentido de Liskov , portanto, você deve pensar em outras maneiras de atender às necessidades de seus stakeholders (como a introdução de uma nova especificação Y, implementada pelo X2).
Para informações mais detalhadas, consulte Pieter Hinjens: o fim das versões de software ou Rich Hickey Simple Made Easy .
Da perspectiva de A, há uma condição prévia de que o colaborador respeite o contrato X. E sua observação é efetivamente que o teste isolado de A não oferece nenhuma garantia de que A reconheça colaboradores que violam o contrato X.
Rever testes integrados são uma farsa ; em alto nível, espera-se que você tenha quantos testes isolados necessários para garantir que X2 implemente o contrato X corretamente e quantos testes isolados necessários para garantir que A faça a coisa certa, com respostas interessantes de um X, e um número menor de testes integrados para garantir que X2 e A concordem com o que X significa.
Você verá algumas vezes essa distinção expressa como testes solitários versus
sociable
testes; veja Jay Fields trabalhando efetivamente com testes de unidade .Novamente, ver testes integrados é uma farsa - Rainsberger descreve em detalhes um ciclo de feedback positivo que é comum (em suas experiências) a projetos que contam com testes integrados (ortografia de nota). Em resumo, sem os testes isolados / solitários aplicando pressão no projeto , a qualidade diminui, levando a mais erros e testes mais integrados ....
Você também precisará de (alguns) testes de integração. Além da complexidade introduzida por vários módulos, a execução desses testes tende a ter mais resistência do que os testes isolados; é mais eficiente repetir verificações muito rápidas quando o trabalho está em andamento, salvando as verificações adicionais para quando você pensa que está "pronto".
fonte
Deixe-me começar dizendo que a premissa básica da questão é falha.
Você nunca está testando (ou zombando) implementações, você está testando (e zombando) interfaces .
Se eu tiver uma classe X real que implemente a interface X1, posso escrever um XM falso que também esteja em conformidade com o X1. Então minha classe A deve usar algo que implemente X1, que pode ser classe X ou zombar de XM.
Agora, suponha que alteremos o X para implementar uma nova interface X2. Bem, obviamente meu código não compila mais. A requer algo que implementa X1 e que não existe mais. O problema foi identificado e pode ser corrigido.
Suponha que, em vez de substituir o X1, apenas o modifiquemos. Agora a classe A está pronta. No entanto, o XM falso não implementa mais a interface X1. O problema foi identificado e pode ser corrigido.
Toda a base para testes de unidade e zombaria é que você escreve um código que usa interfaces. Um consumidor de uma interface não se importa como o código é implementado, apenas com o qual o mesmo contrato é respeitado (entradas / saídas).
Isso ocorre quando seus métodos têm efeitos colaterais, mas acho que isso pode ser excluído com segurança porque "não pode ser testado ou ridicularizado".
fonte
Tomando suas perguntas por sua vez:
Eles são baratos para escrever e executar, e você obtém feedback antecipado. Se você quebrar o X, descobrirá mais ou menos imediatamente se tiver bons testes. Nem pense em escrever testes de integração, a menos que você tenha testado todas as suas camadas (sim, mesmo no banco de dados).
Ter testes aprovados pode realmente dizer muito pouco. Você pode não ter escrito testes suficientes. Você pode não ter testado cenários suficientes. A cobertura de código pode ajudar aqui, mas não é uma bala de prata. Você pode ter testes que sempre passam. Daí o vermelho ser o primeiro passo muitas vezes esquecido do refatorar vermelho, verde.
Mais testes - embora as ferramentas estejam ficando cada vez melhores. MAS você deve definir o comportamento da classe em uma interface (veja abaixo). Nota: sempre haverá um local para testes manuais no topo da pirâmide de testes.
Cada vez mais testes de integração não são a resposta, eles são caros para escrever, executar e manter. Dependendo da configuração da sua compilação, seu gerente de compilação pode excluí-los de qualquer maneira, tornando-os dependentes da lembrança de um desenvolvedor (nunca é uma coisa boa!).
Vi desenvolvedores passarem horas tentando corrigir testes de integração quebrados que teriam encontrado em cinco minutos se tivessem bons testes de unidade. Caso contrário, tente executar o software - é com isso que seus usuários finais se preocupam. Não adianta ter um milhão de testes de unidade que passam se todo o castelo de cartas cai quando o usuário executa o conjunto inteiro.
Se você deseja garantir que a classe A consome a classe X da mesma maneira, você deve usar uma interface em vez de uma concretização. Em seguida, é provável que uma mudança de quebra seja detectada no momento da compilação.
fonte
Está correto.
Os testes de unidade existem para testar a funcionalidade isolada de uma unidade, uma verificação à primeira vista de que funciona da maneira pretendida e não contém erros estúpidos.
Os testes de unidade não existem para testar se o aplicativo inteiro funciona.
O que muita gente esquece é que os testes de unidade são apenas os meios mais rápidos e sujos de validar seu código. Depois de saber que suas pequenas rotinas funcionam, você também precisará executar os testes de integração. O teste de unidade por si só é marginalmente melhor do que nenhum teste.
A razão pela qual temos testes de unidade é que eles devem ser baratos. Rápido para criar, executar e manter. Depois de começar a transformá-los em min testes de integração, você estará em um mundo de dor. Você também pode fazer o teste de integração completo e ignorar completamente o teste de unidade, se quiser fazer isso.
agora, algumas pessoas pensam que uma unidade não é apenas uma função de uma classe, mas toda a classe em si (inclusive eu). No entanto, tudo isso faz é aumentar o tamanho da unidade, para que você possa precisar de menos testes de integração, mas ainda precisa. Ainda é impossível verificar se o seu programa faz o que deve fazer sem um conjunto completo de testes de integração.
e, em seguida, você ainda precisará executar o teste de integração completo em um sistema ativo (ou semi-ativo) para verificar se ele funciona com as condições que o cliente usa.
fonte
Os testes de unidade não comprovam a exatidão de nada. Isso é verdade para todos os testes. Normalmente, os testes de unidade são combinados com o design baseado em contrato (o design por contrato é outra maneira de dizer isso) e, possivelmente, provas automatizadas de correção, se a correção precisar ser verificada regularmente.
Se você possui contratos reais, consistindo em invariantes de classe, pré-condições e pós-condições, é possível provar a correção hierarquicamente, baseando a correção dos componentes de nível superior nos contratos dos componentes de nível inferior. Este é o conceito fundamental por trás do design por contrato.
fonte
fac(5) == 120
, você não provou quefac()
realmente retorna o fatorial de seu argumento. Você só provou quefac()
retorna o fatorial de cinco quando passa5
. E mesmo isso não é certo, como éfac()
possível retornar42
nas primeiras segundas-feiras após um eclipse total em Timbuktu ... O problema aqui é que você não pode provar a conformidade verificando entradas de teste individuais, seria necessário verificar todas as entradas possíveis, e também prove que você não esqueceu nada (como ler o relógio do sistema).Acho testes muito ridículos raramente úteis. Na maioria das vezes, acabo reimplementando o comportamento que a classe original já possui, o que derrota totalmente o objetivo da zombaria.
Para mim, uma estratégia melhor é ter uma boa separação de preocupações (por exemplo, você pode testar a Parte A do seu aplicativo sem inserir as partes B a Z). Uma arquitetura tão boa realmente ajuda a escrever um bom teste.
Além disso, estou mais do que disposto a aceitar efeitos colaterais, desde que eu possa revertê-los, por exemplo, se meu método modificar dados no banco de dados, deixe-o! Contanto que eu possa reverter o banco de dados para o estado anterior, qual é o problema? Além disso, há o benefício que meu teste pode verificar se os dados têm a aparência esperada. DBs na memória ou versões de teste específicas do dbs realmente ajudam aqui (por exemplo, versão de teste na memória do RavenDBs).
Por fim, gosto de zombar dos limites do serviço, por exemplo, não faça essa chamada http para o serviço b, mas vamos interceptá-lo e introduzir um método apropriado.
fonte
Eu gostaria que as pessoas de ambos os campos entendessem que testes de classe e testes de comportamento não são ortogonais.
Os testes de classe e de unidade são usados de forma intercambiável e talvez não devam ser. Por acaso, alguns testes de unidade são implementados nas aulas. Isso é tudo. O teste de unidade acontece há décadas em idiomas sem classes.
Quanto aos comportamentos de teste, é perfeitamente possível fazer isso dentro dos testes de classe usando a construção GWT.
Além disso, se seus testes automatizados prosseguem ao longo das linhas de classe ou de comportamento depende das suas prioridades. Alguns precisarão prototipar rapidamente e obter algo fora da porta, enquanto outros terão restrições de cobertura devido a estilos internos. Muitas razões. Ambas são abordagens perfeitamente válidas. Você paga seu dinheiro, faz sua escolha.
Então, o que fazer quando o código for quebrado. Se foi codificado para uma interface, apenas a concreção precisa ser alterada (junto com todos os testes).
No entanto, a introdução de um novo comportamento não precisa comprometer o sistema. Linux e outros estão cheios de recursos obsoletos. E coisas como construtores (e métodos) podem ser sobrecarregados de maneira feliz sem forçar a mudança de todo o código de chamada.
Onde o teste de classe vence é onde você precisa fazer uma alteração em uma classe que ainda não foi implementada (devido a restrições de tempo, complexidade ou qualquer outra coisa). É muito mais fácil começar uma aula se ela tiver testes abrangentes.
fonte
A menos que a interface do X tenha sido alterada, você não precisa alterar o teste de unidade para A porque nada relacionado a A mudou. Parece que você realmente escreveu um teste de unidade de X e A juntos, mas chamou de teste de unidade de A:
Idealmente, a simulação de X deve simular todos os comportamentos possíveis de X, não apenas o comportamento que você espera de X. Portanto, não importa o que você realmente implemente em X, A já deve ser capaz de lidar com isso. Portanto, nenhuma alteração em X, além de alterar a própria interface, terá qualquer efeito no teste de unidade para A.
Por exemplo: Suponha que A seja um algoritmo de classificação e A forneça dados a serem classificados. A simulação de X deve fornecer um valor de retorno nulo, uma lista vazia de dados, um único elemento, vários elementos já classificados, vários elementos ainda não classificados, vários elementos já classificados, vários elementos ordenados ao contrário, listas com o mesmo elemento repetido, valores nulos misturados, um ridiculamente grande número de elementos e também deve lançar uma exceção.
Então, talvez X inicialmente retornasse dados classificados às segundas-feiras e listas vazias às terças-feiras. Mas agora, quando X retorna dados não classificados às segundas-feiras e lança exceções às terças-feiras, A não se importa - esses cenários já foram abordados no teste de unidade de A.
fonte
Você tem que olhar para diferentes testes.
Os testes de unidade em si só testam o X. Eles estão lá para impedir que você altere o comportamento do X, mas não protegem o sistema inteiro. Eles garantem que você possa refatorar sua classe sem introduzir uma mudança de comportamento. E se você quebrar X, você quebrou ...
O A deve de fato zombar do X para seus testes de unidade e o teste com o Mock deve continuar passando mesmo depois que você o altera.
Mas há mais de um nível de teste! Existem também testes de integração. Esses testes existem para validar a interação entre as classes. Esses testes geralmente têm um preço mais alto, pois não usam zombaria para tudo. Por exemplo, um teste de integração pode realmente gravar um registro em um banco de dados, e um teste de unidade não deve ter dependências externas.
Além disso, se X precisa ter um novo comportamento, seria melhor fornecer um novo método que forneça o resultado desejado
fonte