Devo testar minhas subclasses ou minha classe pai abstrata?

12

Eu tenho uma implementação esquelética, como no Item 18 do Java Efetivo (discussão estendida aqui ). É uma classe abstrata que fornece 2 métodos públicos methodA () e methodB () que chamam subclasses de métodos para "preencher as lacunas" que não posso definir de maneira abstrata.

Eu o desenvolvi primeiro criando uma classe concreta e escrevendo testes de unidade para ele. Quando a segunda classe chegou, eu pude extrair o comportamento comum, implementar as "lacunas ausentes" na segunda classe e ela estava pronta (é claro, os testes de unidade foram criados para a segunda subclasse).

O tempo passou e agora tenho 4 subclasses, cada uma implementando 3 métodos curtos e protegidos que são muito específicos para sua implementação concreta, enquanto a implementação esquelética faz todo o trabalho genérico.

Meu problema é que, quando crio uma nova implementação, escrevo os testes novamente:

  • A subclasse chama um determinado método necessário?
  • Um determinado método possui uma anotação?
  • Eu obtenho os resultados esperados do método A?
  • Eu obtenho os resultados esperados do método B?

Embora veja vantagens com essa abordagem:

  • Ele documenta os requisitos da subclasse através de testes
  • Pode falhar rapidamente em caso de refatoração problemática
  • Teste a subclasse como um todo e por meio de sua API pública ("methodA () funciona?" E não "este método protegido funciona?")

O problema que tenho é que os testes para uma nova subclasse são basicamente simples: - Os testes são todos iguais em todas as subclasses, eles apenas alteram o tipo de retorno dos métodos (a implementação do esqueleto é genérica) e as propriedades que eu verifique a parte afirmativa do teste. - Normalmente, as subclasses não têm testes específicos para elas

Gosto da maneira como os testes se concentram no resultado da subclasse e a protegem da refatoração, mas o fato de implementar o teste se tornar apenas "trabalho manual" me faz pensar que estou fazendo algo errado.

Esse problema é comum ao testar a hierarquia de classes? Como posso evitar isso?

ps: Pensei em testar a classe esqueleto, mas também parece estranho criar um mock de uma classe abstrata para poder testá-la. E acho que qualquer refatoração na classe abstrata que mude o comportamento que não é esperado pela subclasse não seria percebida tão rapidamente. Minha coragem me diz que testar a subclasse "como um todo" é a abordagem preferida aqui, mas fique à vontade para me dizer que estou errado :)

ps2: pesquisei no Google e encontrei muitas perguntas sobre o assunto. Um deles é esse que tem uma ótima resposta de Nigel Thorne, meu caso seria o "número 1" dele. Isso seria ótimo, mas não posso refatorar neste momento, então teria que conviver com esse problema. Se eu pudesse refatorar, teria cada subclasse como estratégia. Tudo bem, mas eu ainda testaria a "classe principal" e as estratégias, mas não notaria nenhuma refatoração que rompa a integração entre a classe principal e as estratégias.

ps3: Encontrei algumas respostas dizendo que "é aceitável" testar a classe abstrata. Concordo que isso é aceitável, mas gostaria de saber qual é a abordagem preferida neste caso (ainda estou começando com testes de unidade)

JSBach
fonte
2
Escreva uma classe de teste abstrata e herde seus testes concretos, espelhando a estrutura do código comercial. Os testes devem testar o comportamento e não a implementação de uma unidade, mas isso não significa que você não pode usar seu conhecimento interno sobre o sistema para atingir esse objetivo com mais eficiência.
Kilian Foth
Eu li em algum lugar que não se deve usar a herança em testes, eu estou procurando a fonte lê-lo com cuidado
JSBach
4
@KilianFoth você tem certeza sobre este conselho? Isso soa como uma receita para testes de unidade frágeis, altamente sensíveis às alterações no projeto e, portanto, não são muito fáceis de manter.
Konrad Morawski

Respostas:

5

Não vejo como você escreveria testes para uma classe base abstrata ; você não pode instanciar, portanto não pode ter uma instância para testar. Você pode criar uma subclasse concreta "fictícia" apenas para fins de testá-la - sem ter certeza do quanto isso seria útil. YMMV.

Toda vez que você cria uma nova implementação (classe), deve escrever testes que exercem essa nova implementação. Como partes da funcionalidade (suas "lacunas") são implementadas em cada subclasse, isso é absolutamente necessário; a saída de cada um dos métodos herdados pode (e provavelmente deveria ) ser diferente para cada nova implementação.

Phill W.
fonte
Sim, eles são diferentes (os tipos e o conteúdo são diferentes). Tecnicamente falando, eu seria capaz de zombar dessa classe ou ter uma implementação simples apenas para fins de teste com implementações vazias. Eu não gosto dessa abordagem. Mas o fato de eu não "precisar" pensar em passar nos testes em novas implementações me faz sentir estranho e também me pergunta sobre a confiança que tenho nesse teste (é claro, se eu mudar a classe base de propósito, o testes falharem, o que é uma prova de que eu posso confiar neles)
JSBach
4

Normalmente, você criaria uma classe base para impor uma interface. Você deseja testar essa interface, não os detalhes abaixo. Portanto, você deve criar testes que instanciam cada classe individualmente, mas somente testam através dos métodos da classe base.

As vantagens desse método são que, porque você testou a interface, agora pode refatorar por baixo da interface. Você pode achar que o código em uma classe filho deve estar no pai ou vice-versa. Agora, seus testes provam que você não quebrou o comportamento ao mover esse código.

Em geral, você deve testar o código como ele deve ser usado. Isso significa evitar o teste de métodos particulares e geralmente significa que sua unidade em teste pode ser um pouco maior do que você pensava originalmente - talvez um grupo de classes em vez de uma única classe. Seu objetivo deve ser isolar as alterações para que você raramente precise alterar um teste ao refatorar em um determinado escopo.

Michael K
fonte