Como você detecta problemas de dependência com testes de unidade quando usa objetos simulados?

95

Você tem uma classe X e escreve alguns testes de unidade que verificam o comportamento X1. Há também a classe A, que leva o X como dependência.

Quando você escreve testes de unidade para A, você zomba de X. Em outras palavras, enquanto a unidade testa A, você define (postula) que o comportamento da simulação de X seja X1. O tempo passa, as pessoas usam seu sistema, precisam mudar, o X evolui: você modifica o X para mostrar o comportamento X2. Obviamente, os testes de unidade para X falharão e você precisará adaptá-los.

Mas e com A? Testes de unidade para A não falharão quando o comportamento de X for modificado (devido ao escarnecimento 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 propósito do teste de unidade", mas que valor tem o teste de unidade? Será que isso apenas lhe diz que, quando todos os testes passam, você não introduziu uma mudança inesperada? E quando o comportamento de algumas classes muda (voluntária ou involuntariamente), como você pode detectar (de preferência de maneira automatizada) todas as conseqüências? Não deveríamos nos concentrar mais nos testes de integração?

    
por bvgheluwe 27.03.2018 / 13:53
fonte

11 respostas

125

When you write unit tests for A, you mock X

Você? Eu não, a menos que seja absolutamente necessário. Eu tenho que se:

  1. X é lento ou
  2. X tem efeitos colaterais

Se nenhum deles se aplicar, então meus testes unitários de A testarão X também. Fazer qualquer outra coisa seria levar os testes de isolamento a um extremo ilógico.

Se você tem partes do seu código usando zombarias de outras partes do seu código, então eu concordo: qual é o sentido de tais testes unitários? 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 se aborrecerem com você chamando esses testes, "testes unitários", então chame-os de "testes automatizados" e continue escrevendo bons testes automatizados.

    
por 27.03.2018 / 14:29
fonte
77

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 estejam conectados corretamente. O problema de confiar apenas no teste de integração é a explosão combinatória resultante das interações entre todas as suas unidades.

Digamos que você tenha classe A, que requer 10 testes de unidade para cobrir todos os caminhos. Então você tem outra classe B, que também requer 10 testes unitários para cobrir todos os caminhos que o código pode percorrer. Agora, digamos que na sua aplicação, você precisa alimentar a saída de A em B. Agora seu código pode pegar 100 caminhos diferentes da entrada de A para a saída de B.

Com testes de unidade, você só precisa de 20 testes de unidade + 1 teste de integração para cobrir completamente todos os casos.

Com 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 em testes de integração Testes Integrados do JB Rainsberger são um Scam HD

    
por 27.03.2018 / 14:22
fonte
72

When you write unit tests for A, you mock X. In other words, while unit testing A, you set (postulate) the behaviour of X's mock to be X1. Time goes by, people do use your system, needs change, X evolves: you modify X to show behaviour X2. Obviously, unit tests for X will fail and you will need to adapt them.

Woah, espere um momento. As implicações dos testes para a falha do X são importantes demais para encobrir isso.

Se a alteração da implementação de X de X1 para X2 interromper os testes de unidade para X, isso indica que você fez uma alteração incompatível com versões anteriores do contrato X.

X2 não é um X, no Liskov sentido , então você deve estar pensando sobre outras maneiras de atender às necessidades de seus interessados (como a introdução de uma nova especificação Y, implementada por X2).

Para insights mais profundos, veja Pieter Hinjens: O fim das versões de software , ou Rich Hickey Simples Made Easy .

Do ponto de vista de A, há uma condição prévia de que o colaborador respeita o contrato X. E sua observação é efetivamente que o teste isolado para A não lhe dá nenhuma garantia de que A reconhece colaboradores que violam o contrato X.

Revise Testes integrados são uma farsa ; em alto nível, espera-se que você tenha tantos testes isolados quanto precisar para garantir que o X2 implemente o contrato X corretamente, e tantos testes isolados quantos forem 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.

Às vezes, você verá essa distinção como teste solitário vs sociable tests ; veja Jay Fields Trabalhando efetivamente com testes unitários .

Shouldn't we focus more on integration testing?

Mais uma vez, ver testes integrados são uma fraude - Rainsberger descreve em detalhes um ciclo de feedback positivo que é comum (em suas experiências) a projetos que estão confiando em testes integrados (com notas ortográficas). Em resumo, sem os testes isolados / solitários aplicando pressão ao projeto , a qualidade se degrada, levando a mais erros e testes mais integrados ...

Você também precisará de (alguns) testes de integração. Além da complexidade introduzida por múltiplos módulos, a execução desses testes tende a ter mais arrasto do que os testes isolados; é mais eficiente fazer iterações em verificações muito rápidas quando o trabalho está em andamento, salvando as verificações adicionais para quando você acha que está "pronto".

    
por 27.03.2018 / 16:01
fonte
15

Deixe-me começar dizendo que a premissa central da questão é falha.

Você nunca está testando (ou zombando) implementações, você está testando (e zombando) interfaces .

Se eu tiver uma classe real X que implemente a interface X1, posso escrever um XM simulado que também esteja em conformidade com X1. Então minha classe A deve usar algo que implemente X1, que pode ser da classe X ou do mock 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 implemente X1 e que não exista 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 mock XM não implementa mais a interface X1. O problema foi identificado e pode ser corrigido.

Toda a base para testes unitários e zombarias é que você escreve código que usa interfaces. Um consumidor de uma interface não se importa como o código é implementado, apenas que o mesmo contrato é respeitado (entradas / saídas).

Isso é interrompido quando seus métodos têm efeitos colaterais, mas acho que isso pode ser excluído com segurança como "não pode ser testado ou ridicularizado pela unidade".

    
por 27.03.2018 / 19:40
fonte
9

Responda às suas perguntas:

what value does unit testing have then

Eles são baratos para escrever e executar e você recebe feedback antecipado. Se você quebrar o X, você 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, até mesmo no banco de dados).

Does it really only tell you that when all tests pass, you haven't introduced a breaking change

Ter testes aprovados pode dizer muito pouco. Você pode não ter escrito testes suficientes. Você pode não ter testado cenários suficientes. 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 frequentemente ignorado do refator vermelho, verde.

And when some class's behaviour changes (willingly or unwillingly), how can you detect (preferably in an automated way) all the consequences

Mais testes - embora as ferramentas estejam cada vez melhores. MAS você deve estar definindo o comportamento da classe em uma interface (veja abaixo). N.B. sempre haverá um lugar para testes manuais sobre a pirâmide de testes.

Shouldn't we focus more on integration testing?

Cada vez mais testes de integração não são a resposta, eles são caros para escrever, executar e manter. Dependendo de sua configuração de compilação, seu gerenciador de construção pode excluí-los de qualquer maneira, tornando-os dependentes de uma lembrança do desenvolvedor (nunca é uma coisa boa!).

Eu vi desenvolvedores gastarem horas tentando corrigir testes de integração quebrados que teriam encontrado em cinco minutos se tivessem bons testes de unidade. Caso contrário, tente apenas executar o software - isso é tudo o que seus usuários finais vão se importar. Não adianta ter milhões de testes de unidade que passam quando a casa inteira de cartas cai quando o usuário executa a suíte inteira.

Se você quer ter certeza de que a classe A consome a classe X da mesma maneira, você deveria estar usando uma interface ao invés de uma concreção. Em seguida, é mais provável que uma alteração de quebra seja detectada em tempo de compilação.

    
por 27.03.2018 / 14:24
fonte
9

Está correto.

Testes unitários estão lá para testar a funcionalidade isolada de uma unidade, uma verificação inicial de que ela funciona conforme o esperado e não contém erros estúpidos.

Testes de unidade não estão lá para testar se o aplicativo inteiro funciona.

O que muita gente esquece é que os testes de unidade são apenas o meio mais rápido e sujo de validar seu código. Depois que você souber que suas pequenas rotinas funcionam, você terá que executar os testes de integração também. O teste unitário por si só é apenas marginalmente melhor que nenhum teste.

A razão pela qual temos testes unitários é que eles são supostamente baratos. Rápido para criar e executar e manter. Uma vez que você comece a transformá-los em min testes de integração, você está em um mundo de dor. Você também pode fazer um teste de Integração completo e ignorar completamente o teste de unidade se for fazer isso.

agora, algumas pessoas pensam que uma unidade não é apenas uma função de uma turma, mas toda a turma em si (inclusive eu). No entanto, tudo isso aumenta o tamanho da unidade, portanto, você pode precisar de menos testes de integração, mas ainda precisa dela. Ainda é impossível verificar se o seu programa faz o que é necessário sem um conjunto de testes de integração completa.

e, em seguida, você ainda precisará executar o teste de integração completo em um sistema ao vivo (ou semi-ativo) para verificar se ele funciona com as condições que o cliente usa.

    
por 28.03.2018 / 18:34
fonte
2

Testes de unidade não provam a exatidão de nada. Isto é verdade para todos os testes. Normalmente, os testes unitários são combinados com o projeto baseado em contrato (o design por contrato é outra maneira de dizê-lo) e, possivelmente, as provas automatizadas de correção, se a correção precisa ser verificada regularmente.

Se você tiver contratos reais, consistindo em invariantes de classe, pré-condições e pós-condições, é possível comprovar a correção hierarquicamente, baseando a correção de componentes de nível superior nos contratos de componentes de nível inferior. Este é o conceito fundamental por trás do projeto por contrato.

    
por 27.03.2018 / 20:33
fonte
2

Acho que testes altamente ridicularizados raramente são úteis. Na maior parte do tempo, acabo reimplementando o comportamento que a classe original já possui, o que anula totalmente o propósito de zombar.

M.e. Uma estratégia melhor é ter uma boa separação de interesses (por exemplo, você pode testar a Parte A do seu aplicativo sem trazer as partes B a Z). Uma arquitetura tão boa realmente ajuda a escrever bons testes.

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! Contanto que eu possa reverter o banco de dados para o estado anterior, qual é o problema? Além disso, há o benefício de que meu teste possa verificar se os dados estão como esperados. BDs In-Memory ou versões de teste específicas de dbs realmente ajudam aqui (por exemplo, a versão de teste in-memory do RavenDB).

Por fim, gosto de fazer escárnio em limites de serviço, por exemplo não faça essa chamada http para o serviço b, mas vamos interceptá-lo e introduzir um adequado

    
por 31.03.2018 / 07:19
fonte
1

Eu gostaria que as pessoas em 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. Alguns testes unitários só são implementados em classes. Isso é tudo. Testes de unidade ocorreram por décadas em idiomas sem aulas.

Quanto a testar comportamentos, é perfeitamente possível fazer isso dentro do teste de classe usando a construção do GWT.

Além disso, se seus testes automatizados prosseguem ao longo das linhas de classe ou de comportamento, isso depende das suas prioridades. Alguns precisarão rapidamente prototipar e fazer algo fora da porta, enquanto outros terão restrições de cobertura devido aos estilos internos. Muitas razões. Ambos são abordagens perfeitamente válidas. Você paga seu dinheiro, você escolhe.

Então, o que fazer quando o código é interrompido. Se ele foi codificado para uma interface, então apenas a concreção precisa mudar (junto com quaisquer testes).

No entanto, a introdução de um novo comportamento não precisa comprometer o sistema. Linux et al estão cheios de recursos obsoletos. E coisas como construtores (e métodos) podem ser facilmente sobrecarregados sem forçar qualquer código de chamada a mudar.

Onde o teste de classe vence é onde você precisa fazer uma mudança em uma classe que ainda não foi instalada (devido a restrições de tempo, complexidade ou qualquer outra coisa). É muito mais fácil começar com uma aula se ela tiver testes abrangentes.

    
por 28.03.2018 / 19:42
fonte
0

A menos que a interface para X seja alterada, você não precisa alterar o teste de unidade para A porque nada relacionado a A foi alterado. Parece que você realmente escreveu um teste unitário de X e A juntos, mas o chamou de teste unitário de A:

When you write unit tests for A, you mock X. In other words, while unit testing A, you set (postulate) the behaviour of X's mock to be X1.

Idealmente, a simulação de X deve simular todos os possíveis comportamentos de X, não apenas o comportamento que você espera do X. Portanto, não importa o que você realmente implemente no X, A já deve ser capaz de lidar com isso. Portanto, nenhuma alteração no X, além de alterar a interface em si, terá algum 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 classificados para trás, 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.

Assim, talvez X tenha retornado inicialmente os dados classificados às segundas-feiras e as 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 unitário de A.

    
por 28.03.2018 / 16:50
fonte
-2

Você precisa analisar diferentes testes.

Testes de unidade em si só testarão X. Eles estão lá para evitar que você mude o comportamento de X, mas não proteja todo o sistema. Eles garantem que você possa refatorar sua turma sem introduzir uma mudança no comportamento. E se você quebrar X, você quebrou ...

Um deve simular o X para seus testes de unidade e o teste com o Mock deve continuar passando mesmo depois que você o alterar.

Mas há mais de um nível de teste! Existem também testes de integração. Esses testes estão lá para validar a interação entre as classes. Esses testes geralmente têm um preço mais alto, já que não usam mocks 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 precisar ter um novo comportamento, seria melhor fornecer um novo método que fornecesse o resultado desejado

    
por 29.03.2018 / 10:08
fonte