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.
fonte
Respostas:
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.
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.
fonte
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.
fonte
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.
fonte