Testes de unidade quebradiços devido à necessidade de zombaria excessiva

21

Eu tenho lutado com um problema cada vez mais irritante em relação aos nossos testes de unidade que estamos implementando em minha equipe. Estamos tentando adicionar testes de unidade ao código legado que não foi bem projetado e, embora não tenhamos tido nenhuma dificuldade com a adição real dos testes, estamos começando a ter dificuldades com o desempenho dos testes.

Como exemplo do problema, digamos que você tenha um método que chama 5 outros métodos como parte de sua execução. Um teste para esse método pode ser para confirmar que um comportamento ocorre como resultado de um desses 5 outros métodos serem chamados. Portanto, como um teste de unidade deve falhar por um motivo e apenas um motivo, você deseja eliminar possíveis problemas causados ​​por chamar esses outros 4 métodos e zombá-los. Ótimo! O teste de unidade é executado, os métodos simulados são ignorados (e seu comportamento pode ser confirmado como parte de outros testes de unidade) e a verificação funciona.

Mas há um novo problema - o teste de unidade tem um conhecimento profundo de como você confirmou que o comportamento e qualquer assinatura são alterados para qualquer um dos outros 4 métodos no futuro, ou quaisquer novos métodos que precisem ser adicionados ao 'método pai' resulta em necessidade de alterar o teste de unidade para evitar possíveis falhas.

Naturalmente, o problema poderia ser atenuado de alguma maneira, simplesmente com mais métodos realizando menos comportamentos, mas eu esperava que houvesse talvez uma solução mais elegante disponível.

Aqui está um exemplo de teste de unidade que captura o problema.

Como uma observação rápida, 'MergeTests' é uma classe de teste de unidade que herda da classe que estamos testando e substitui o comportamento conforme necessário. Esse é um 'padrão' que empregamos em nossos testes para nos permitir substituir chamadas para classes / dependências externas.

[TestMethod]
public void VerifyMergeStopsSpinner()
{
    var mockViewModel = new Mock<MergeTests> { CallBase = true };
    var mockMergeInfo = new MergeInfo(Mock.Of<IClaim>(), Mock.Of<IClaim>(), It.IsAny<bool>());

    mockViewModel.Setup(m => m.ClaimView).Returns(Mock.Of<IClaimView>);
    mockViewModel.Setup(
        m =>
        m.TryMergeClaims(It.IsAny<Func<bool>>(), It.IsAny<IClaim>(), It.IsAny<IClaim>(), It.IsAny<bool>(),
                         It.IsAny<bool>()));
    mockViewModel.Setup(m => m.GetSourceClaimAndTargetClaimByMergeState(It.IsAny<MergeState>())).Returns(mockMergeInfo);
    mockViewModel.Setup(m => m.SwitchToOverviewTab());
    mockViewModel.Setup(m => m.IncrementSaveRequiredNotification());
    mockViewModel.Setup(m => m.OnValidateAndSaveAll(It.IsAny<object>()));
    mockViewModel.Setup(m => m.ProcessPendingActions(It.IsAny<string>()));

    mockViewModel.Object.OnMerge(It.IsAny<MergeState>());    

    mockViewModel.Verify(mvm => mvm.StopSpinner(), Times.Once());
}

Como o resto de vocês lidou com isso ou não existe uma maneira "simples" de lidar com isso?

Atualização - Agradeço o feedback de todos. Infelizmente, e não é nenhuma surpresa realmente, não parece haver uma ótima solução, padrão ou prática que se possa seguir nos testes de unidade se o código que está sendo testado for ruim. Marquei a resposta que melhor capturou essa verdade simples.

PremiumTier
fonte
Uau, eu só vejo configurações falsas, nenhuma instanciação do SUT ou qualquer coisa, você está testando alguma implementação real aqui? Quem deve ligar para o StopSpinner? OnMerge? Você deve zombar de quaisquer dependências que possam ser chamadas, mas não a coisa em si .. #
315
É um pouco difícil de ver, mas o Mock <MergeTests> é o SUT. Definimos o sinalizador CallBase para garantir que o método 'OnMerge' seja executado no objeto real, mas simulamos métodos chamados por 'OnMerge' que poderiam causar falha no teste devido a problemas de dependência etc. O objetivo do teste é a última linha - para verificar que paramos o controle giratório neste caso.
PremiumTier
Os MergeTests parecem outra classe instrumentada, e não algo que vive em produção, daí a confusão.
Joppe
1
Completamente fora de seus outros problemas, parece-me errado que seu SUT seja um <MergeTests> Mock. Por que você testaria uma zombaria? Por que você não está testando a própria classe MergeTests?
Eric Rei

Respostas:

18
  1. Corrija o código para ter um design melhor. Se seus testes tiverem esses problemas, seu código terá problemas piores quando você tentar mudar as coisas.

  2. Se você não pode, talvez precise ser menos ideal. Teste contra as condições pré e pós-método. Quem se importa se você estiver usando os outros 5 métodos? Presumivelmente, eles têm seus próprios testes de unidade, deixando claro (er) o que causou a falha quando os testes falharam.

"testes de unidade devem ter apenas um motivo para falhar" é uma boa orientação, mas, na minha experiência, impraticável. Os testes difíceis de escrever não são escritos. Testes frágeis não são acreditados.

Telastyn
fonte
Eu concordo completamente com a fixação do design do código, mas no mundo menos ideal para o desenvolvimento de uma grande empresa com prazos apertados, pode ser difícil defender o "pagamento" da dívida técnica incorrida pelas equipes anteriores ou decisões erradas, tudo isso uma vez. Para o seu segundo ponto, grande parte da zombaria não é apenas porque queremos que o teste falhe apenas por uma razão - é porque o código que está sendo executado não pode ser executado sem antes também lidar com um grande número de dependências criadas dentro desse código. . Desculpe por mover as postagens dessa meta.
PremiumTier
Se um design melhor não for realista, concordo com 'Quem se importa se você estiver usando os outros 5 métodos?' Verifique se o método executa a função necessária, não como está fazendo isso.
Kwebble # 8/13
@ Kwebble - Entendido, no entanto, o objetivo da pergunta era determinar se havia uma maneira simples de verificar o comportamento de um método, quando você também precisa zombar de outros comportamentos chamados dentro do método para executar o teste. Quero remover o 'como', mas não sei como :) :)
PremiumTier
Não há bala mágica de prata. Não existe uma "maneira simples" de testar código ruim. O código em teste precisa ser refatorado ou o próprio código de teste também será ruim. O teste será ruim, porque será específico demais para os detalhes internos, conforme você se deparou com , ou conforme sugerido portil , que você pode executar os testes em um ambiente de trabalho, mas os testes serão muito mais lentos e mais complexos. De qualquer forma, os testes serão mais difíceis de escrever, mais difíceis de manter e propensos a falsos negativos.
Steven Doggart
8

Dividir métodos grandes em métodos pequenos mais focados é definitivamente uma prática recomendada. Você vê isso como uma dor na verificação do comportamento do teste de unidade, mas também está sofrendo de outras formas.

Dito isto, é uma heresia, mas sou pessoalmente fã de criar ambientes de teste temporários realistas. Ou seja, em vez de zombar de tudo o que está oculto dentro desses outros métodos, verifique se existe um ambiente temporário fácil de configurar (completo com esquemas e bancos de dados privados - o SQLite pode ajudar aqui) que permite executar todas essas coisas. A responsabilidade de saber como criar / desmembrar esse ambiente de teste vive com o código que exige isso, para que, quando ele for alterado, você não precise alterar todo o código de teste de unidade que dependia de sua existência.

Mas noto que isso é uma heresia da minha parte. As pessoas que participam muito de testes de unidade defendem testes de unidade "puros" e chamam o que eu descrevi de "testes de integração". Pessoalmente, não me preocupo com essa distinção.

btilly
fonte
3

Eu consideraria facilitar as zombarias e apenas formular testes que possam incluir os métodos que chama.

Não teste o como , teste o quê . É o resultado que importa, inclua os sub-métodos, se necessário.

De outro ângulo, você pode formular um teste, fazê-lo passar com um grande método, refatorar e terminar com uma árvore de métodos após a refatoração. Você não precisa testar todos e cada um deles isoladamente. É o resultado final que conta.

Se os sub-métodos dificultarem o teste de alguns aspectos, considere dividi-los em classes separadas para que você possa zombar deles de maneira mais limpa, sem que sua classe em teste seja fortemente instrumentada / costurada. É meio difícil dizer se você está realmente testando alguma implementação concreta no seu exemplo de teste.

Joppe
fonte
O problema é que temos que zombar do 'como' para testar o 'o quê'. É uma limitação imposta pelo design do código. Certamente, não desejo zombar de como isso é o que torna o teste frágil.
PremiumTier
Olhando para os nomes dos métodos, acho que sua classe testada está assumindo muitas responsabilidades. Leia o princípio da responsabilidade única. Empréstimos do MVC podem ajudar um pouco, sua classe parece lidar com questões de interface do usuário, infraestrutura e negócios.
Joppe
Sim :( Isso seria que o código legado mal projetada eu estava me referindo Estamos trabalhando na re-design e refatorar, mas sentimos que seria melhor colocar a fonte ensaiada em primeiro lugar..
PremiumTier