A espionagem nas aulas testadas é uma má prática?

13

Estou trabalhando em um projeto em que chamadas internas de classe são comuns, mas os resultados são muitas vezes valores simples. Exemplo ( código não real ):

public boolean findError(Set<Thing1> set1, Set<Thing2> set2) {
  if (!checkFirstCondition(set1, set2)) {
    return false;
  }
  if (!checkSecondCondition(set1, set2)) {
    return false;
  }
  return true;
}

Escrever testes de unidade para esse tipo de código é realmente difícil, pois só quero testar o sistema de condições e não a implementação das condições reais. (Eu faço isso em testes separados.) Na verdade, seria melhor se eu passasse em funções que implementam as condições e, em testes, eu simplesmente forneço alguns mock. O problema dessa abordagem é o barulho: usamos muito genéricos .

Uma solução de trabalho; no entanto, é tornar o objeto testado um espião e zombar das chamadas para as funções internas.

systemUnderTest = Mockito.spy(systemUnderTest);
doReturn(true).when(systemUnderTest).checkFirstCondition(....);

A preocupação aqui é que a implementação do SUT seja efetivamente alterada e pode ser problemático manter os testes sincronizados com a implementação. Isso é verdade? Existe prática recomendada para evitar esse estrago de chamadas de método interno?

Observe que estamos falando de partes de um algoritmo; portanto, dividi-lo em várias classes pode não ser uma decisão desejada.

allprog
fonte

Respostas:

15

Os testes de unidade devem tratar as classes que eles testam como caixas pretas. A única coisa que importa é que seus métodos públicos se comportem da maneira que se espera. Como a classe consegue isso através de métodos internos e privados, não importa.

Quando você sente que é impossível criar testes significativos dessa maneira, é um sinal de que suas aulas são poderosas demais e fazem muito. Você deve considerar mover algumas de suas funcionalidades para classes separadas, que podem ser testadas separadamente.

Philipp
fonte
1
Compreendi a ideia dos testes de unidade há muito tempo e escrevi muitos deles com sucesso. É apenas ilusório que algo pareça simples no papel, pareça pior no código e, finalmente, me deparo com algo que tem uma interface realmente simples, mas que exige que eu zombe da metade do mundo em torno das entradas.
allprog
@ allprog Quando você precisa fazer muita zombaria, parece que você tem muitas dependências entre suas classes. Você tentou reduzir o acoplamento entre eles?
Philipp
@ allprog se você estiver nessa situação, o design da classe é o culpado.
itsbruce
É o modelo de dados que causa a dor de cabeça. Ele precisa seguir as regras do ORM e muitos outros requisitos. Com lógica de negócios pura e código sem estado, é muito mais fácil obter os testes de unidade corretamente.
allprog
3
Os testes de unidade não precisam necessariamente lidar com o SUT como backbox. É por isso que eles são chamados de testes de unidade. Ao zombar das dependências, posso influenciar o ambiente e para saber o que tenho que zombar, tenho que conhecer alguns dos internos também. Mas é claro que isso não significa que o SUT deva ser alterado de qualquer forma. A espionagem, no entanto, permite fazer algumas mudanças.
allprog
4

Se ambos findError()e checkFirstCondition()etc. são métodos públicos da sua classe, findError()é efetivamente uma fachada para a funcionalidade que já está disponível na mesma API. Não há nada errado com isso, mas isso significa que você deve escrever testes muito semelhantes aos testes já existentes. Essa duplicação simplesmente reflete a duplicação na sua interface pública. Isso não é motivo para tratar esse método de maneira diferente dos outros.

Kilian Foth
fonte
Os métodos internos são tornados públicos apenas porque precisam ser testáveis ​​e eu não quero subclassificar o SUT ou incluir os testes de unidade na classe SUT como uma classe interna estática. Mas entendi seu ponto. No entanto, não consegui encontrar boas orientações para evitar esse tipo de situação. Os tutoriais sempre ficam presos no nível básico que não tem nada a ver com software real. Caso contrário, o motivo da espionagem é exatamente para evitar a duplicação do código de teste e tornar a unidade de teste com escopo definido.
allprog
3
Discordo que os métodos auxiliares precisam ser públicos para testes de unidade adequados. Se o contrato de um método declarar que ele verifica várias condições, não há nada errado em escrever vários testes no mesmo método público, um para cada "subcontrato". O objetivo dos testes de unidade é obter cobertura de todo o código, não obter uma cobertura superficial de métodos públicos por meio de uma correspondência de teste de método 1: 1.
Kilian Foth
Usar apenas a API pública para teste é muitas vezes significativamente mais complexo do que testar as partes internas uma a uma. Não discuto, entendo que essa abordagem não é a melhor e tem uma retrospectiva que minha pergunta mostra. O maior problema é que as funções não são compostas em Java e as soluções alternativas são extremamente concisas. Mas parece não haver outra solução para testes reais de unidade.
allprog
4

Os testes de unidade devem testar o contrato; é a única coisa importante para eles. Testar qualquer coisa que não faça parte do contrato não é apenas uma perda de tempo, é uma fonte potencial de erro. Sempre que você vê um desenvolvedor alterando os testes quando altera um detalhe de implementação, os alarmes devem tocar; esse desenvolvedor pode estar (intencionalmente ou não) ocultando seus erros. O teste deliberado dos detalhes da implementação força esse mau hábito, aumentando a probabilidade de que os erros sejam mascarados.

As chamadas internas são um detalhe de implementação e só devem ser interessantes para medir o desempenho . O que geralmente não é o trabalho de testes de unidade.

itsbruce
fonte
Parece bom. Mas, na realidade, a "string" que eu tenho que digitar e chamar de código está em uma linguagem que sabe muito pouco sobre funções. Em teoria, posso descrever facilmente um problema e fazer substituições aqui e ali para simplificá-lo. No código, tenho que adicionar muito ruído sintático para obter essa flexibilidade que me impede de usá-lo. Se o método acontiver uma chamada para o método bna mesma classe, os testes de adeverão incluir testes de b. E não há como mudar isso, desde que bnão seja passado para ao parâmetro Mas, não vejo outra solução.
allprog
1
Se bfaz parte da interface pública, deve ser testado de qualquer maneira. Caso contrário, não precisa ser testado. Se você o tornou público apenas porque queria testá-lo, cometeu um erro.
itsbruce
Veja meu comentário na resposta de @ Philip. Ainda não mencionei, mas o modelo de dados é a fonte do mal. Código puro e sem estado é um pedaço de bolo.
allprog
2

Em primeiro lugar, estou me perguntando o que é difícil testar sobre a função de exemplo que você escreveu? Até onde posso ver, você pode simplesmente passar várias entradas e verificar se o valor booleano correto é retornado. o que estou perdendo?

Quanto aos espiões, o tipo de teste chamado "caixa branca" que usa espiões e zombarias exige muito mais trabalho para escrever, não apenas porque há muito mais código de teste para escrever, mas sempre que a implementação é executada alterado, você também deve alterar os testes (mesmo que a interface permaneça a mesma). E esse tipo de teste também é menos confiável que o teste de caixa preta, porque você precisa garantir que todo esse código de teste extra esteja correto e, embora possa confiar, os testes de unidade de caixa preta falharão se não corresponderem à interface , você não pode confiar nisso sobre o uso excessivo de zombarias de código, porque às vezes o teste nem testa muito código real - apenas zombarias. Se as zombarias estiverem incorretas, é provável que seus testes sejam bem-sucedidos, mas seu código ainda está quebrado.

Qualquer pessoa que tenha experiência com testes de caixa branca pode dizer que é um pé no saco escrever e manter. Juntamente com o fato de serem menos confiáveis, os testes em caixa branca são simplesmente muito inferiores na maioria dos casos.

BT
fonte
Obrigado pela observação. A função de exemplo é ordens de magnitude mais simples do que qualquer coisa que você precise escrever em um algoritmo complexo. Na verdade, a questão é mais ou menos assim: é problemático testar algoritmos com espiões em várias partes. Este não é um código com estado, todo o estado é separado em argumentos de entrada. O problema é que eu quero testar a função complexa no exemplo sem precisar fornecer parâmetros sãos para as subfunções.
allprog
Com o surgimento da programação funcional no Java 8, isso se tornou um pouco mais elegante, mas ainda assim manter a funcionalidade em uma única classe pode ser uma escolha melhor no caso de algoritmos, em vez de extrair as partes diferentes (não úteis) para "usar uma vez" classes apenas por causa da testabilidade. Nesse sentido, os espiões fazem o mesmo que zombam, mas sem ter que explodir visualmente o código coerente. Na verdade, o mesmo código de configuração é usado como nas zombarias. Eu gosto de ficar longe de extremos, todo tipo de teste pode ser apropriado em locais específicos. Testar de alguma forma é muito melhor do que não. :)
allprog
"Quero testar a função complexa .. sem ter que fornecer parâmetros sãos para as sub-funções" - não entendo o que você quer dizer com isso. Quais sub funções? Você está falando sobre as funções internas que estão sendo usadas pela 'função complexa'?
BT
É para isso que a espionagem é útil no meu caso. As funções internas são bastante complexas de controlar. Não por causa do código, mas porque eles implementam algo logicamente complexo. Mover coisas em uma classe diferente é uma opção natural, mas essas funções por si só não são úteis. Portanto, manter a classe unida e controlá-la pela funcionalidade de espião acabou sendo uma opção melhor. Funciona perfeitamente há quase um ano e pode facilmente suportar mudanças de modelo. Eu não uso esse padrão desde então, apenas achei bom mencionar que é viável em alguns casos.
allprog
@ allprog "logicamente complexo" - Se é complexo, você precisa de testes complexos. Não há maneira de contornar isso. Os espiões vão tornar as coisas mais difíceis e complexas para você. Você deve criar sub-funções compreensíveis que você pode testar por conta própria, em vez de usar espiões para testar seu comportamento especial dentro de outra função.
BT