Em muitos casos, posso ter uma classe existente com algum comportamento:
class Lion
{
public void Eat(Herbivore herbivore) { ... }
}
... e eu tenho um teste de unidade ...
[TestMethod]
public void Lion_can_eat_herbivore()
{
var herbivore = buildHerbivoreForEating();
var test = BuildLionForTest();
test.Eat(herbivore);
Assert.IsEaten(herbivore);
}
Agora, o que acontece é que preciso criar uma classe Tiger com comportamento idêntico ao do Lion:
class Tiger
{
public void Eat(Herbivore herbivore) { ... }
}
... e como quero o mesmo comportamento, preciso executar o mesmo teste, faço algo assim:
interface IHerbivoreEater
{
void Eat(Herbivore herbivore);
}
... e refatoro meu teste:
[TestMethod]
public void Lion_can_eat_herbivore()
{
IHerbivoreEater_can_eat_herbivore(BuildLionForTest);
}
public void IHerbivoreEater_can_eat_herbivore(Func<IHerbivoreEater> builder)
{
var herbivore = buildHerbivoreForEating();
var test = builder();
test.Eat(herbivore);
Assert.IsEaten(herbivore);
}
... e depois adiciono outro teste para minha nova Tiger
classe:
[TestMethod]
public void Tiger_can_eat_herbivore()
{
IHerbivoreEater_can_eat_herbivore(BuildTigerForTest);
}
... e então refatoro minhas classes Lion
e Tiger
(geralmente por herança, mas às vezes por composição):
class Lion : HerbivoreEater { }
class Tiger : HerbivoreEater { }
abstract class HerbivoreEater : IHerbivoreEater
{
public void Eat(Herbivore herbivore) { ... }
}
... e está tudo bem. No entanto, como a funcionalidade agora está na HerbivoreEater
classe, parece que há algo errado em ter testes para cada um desses comportamentos em cada subclasse. No entanto, são as subclasses que estão realmente sendo consumidas, e é apenas um detalhe da implementação que elas compartilham comportamentos sobrepostos ( Lions
e Tigers
podem ter usos finais totalmente diferentes, por exemplo).
Parece redundante testar o mesmo código várias vezes, mas há casos em que a subclasse pode e substitui a funcionalidade da classe base (sim, isso pode violar o LSP, mas vamos ser sinceros, IHerbivoreEater
é apenas uma interface de teste conveniente - é pode não ser importante para o usuário final). Então esses testes têm algum valor, eu acho.
O que outras pessoas fazem nessa situação? Você acabou de passar seu teste para a classe base ou todas as subclasses para o comportamento esperado?
EDIT :
Com base na resposta do @pdr, acho que devemos considerar isso: o IHerbivoreEater
é apenas um contrato de assinatura de método; não especifica comportamento. Por exemplo:
[TestMethod]
public void Tiger_eats_herbivore_haunches_first()
{
IHerbivoreEater_eats_herbivore_haunches_first(BuildTigerForTest);
}
[TestMethod]
public void Cheetah_eats_herbivore_haunches_first()
{
IHerbivoreEater_eats_herbivore_haunches_first(BuildCheetahForTest);
}
[TestMethod]
public void Lion_eats_herbivore_head_first()
{
IHerbivoreEater_eats_herbivore_head_first(BuildLionForTest);
}
fonte
Animal
classe que contenhaEat
? Todos os animais comem e, portanto, a classeTiger
eLion
poderia herdar do animal.Eat
comportamento na classe base, todas as subclasses devem exibir o mesmoEat
comportamento. No entanto, estou falando de duas classes relativamente independentes que compartilham um comportamento. Considere, por exemplo, oFly
comportamento deBrick
ePerson
que, podemos assumir, exibem um comportamento de vôo semelhante, mas não faz sentido necessariamente que eles derivem de uma classe base comum.Respostas:
Isso é ótimo porque mostra como os testes realmente conduzem da maneira que você pensa sobre o design. Você está detectando problemas no design e fazendo as perguntas certas.
Existem duas maneiras de ver isso.
IHerbivoreEater é um contrato. Todos os IHerbivoreEaters devem ter um método Eat que aceite um herbívoro. Agora, seus testes não se importam como é comido; seu leão pode começar com os quadris e o tigre pode começar pela garganta. Todo o seu teste se importa é que, depois de chamar de Eat, o herbívoro é comido.
Por outro lado, parte do que você está dizendo é que todos os IHerbivoreEaters comem o Herbivore exatamente da mesma maneira (daí a classe base). Sendo esse o caso, não há sentido em ter um contrato IHerbivoreEater. Não oferece nada. Você também pode herdar do HerbivoreEater.
Ou talvez acabe com Lion e Tiger completamente.
Mas, se Leão e Tigre são diferentes em todos os sentidos, exceto em seus hábitos alimentares, então você precisa começar a se perguntar se vai encontrar problemas com uma árvore de herança complexa. E se você também quiser derivar as duas classes de Feline, ou apenas a classe Lion de KingOfItsDomain (junto com Shark, talvez). É aqui que o LSP realmente entra.
Eu sugeriria que o código comum é melhor encapsulado.
O mesmo vale para o Tiger.
Agora, aqui está uma coisa linda se desenvolvendo (linda porque eu não pretendia). Se você apenas disponibilizar esse construtor privado para o teste, poderá passar um IHerbivoreEatingStrategy falso e simplesmente testar se a mensagem é passada corretamente para o objeto encapsulado.
E seu teste complexo, aquele com o qual você estava preocupado, só precisa testar a estratégia StandardHerbivoreEating. Uma classe, um conjunto de testes, sem código duplicado para se preocupar.
E se, mais tarde, você quiser dizer aos tigres que eles devem comer seus herbívoros de uma maneira diferente, nenhum desses testes precisará mudar. Você está simplesmente criando uma nova HerbivoreEatingStrategy e testando isso. A fiação é testada no nível do teste de integração.
fonte
IHerbivoreEater
é um contrato, mas apenas na medida em que eu preciso para testar. Parece-me que este é um caso em que a digitação com patos realmente ajudaria. Eu só quero enviar os dois para a mesma lógica de teste agora. Não acho que a interface prometa o comportamento. Os testes devem fazer isso.De maneira geral, você está perguntando se é apropriado usar o conhecimento da caixa branca para omitir alguns testes. De uma perspectiva de caixa preta,
Lion
eTiger
são classes diferentes. Portanto, alguém sem familiaridade com o código os testaria, mas você com profundo conhecimento de implementação sabe que pode se safar simplesmente testando um animal.Parte do motivo para desenvolver testes de unidade é permitir refatorar mais tarde, mas manter a mesma interface de caixa preta . Os testes de unidade estão ajudando você a garantir que suas aulas continuem cumprindo o contrato com os clientes ou, pelo menos, obriga a realizar e pensar com cuidado como o contrato pode mudar. Você mesmo percebe isso
Lion
ouTiger
pode substituirEat
em algum momento posterior. Se isso for remotamente possível, um teste simples de unidade para testar que cada animal que você apoia pode comer, da seguinte maneira:deve ser muito simples de fazer e suficiente e garantirá que você possa detectar quando os objetos não cumprem o contrato.
fonte
Você está fazendo isso certo. Pense em um teste de unidade como o teste do comportamento de um único uso do seu novo código. É a mesma chamada que você faria do código de produção.
Nessa situação, você está completamente correto; um usuário de um leão ou tigre não vai (nem precisará, pelo menos) se importar com os dois HerbivoreEaters e que o código que realmente executa o método é comum a ambos na classe base. Da mesma forma, um usuário de um resumo do HerbivoreEater (fornecido pelo leão ou tigre concreto) não se importa com o que ele possui. O que eles se preocupam é que sua implementação concreta desconhecida do HerbivoreEater Lion, Tiger ou Eat comerá () um Herbivore corretamente.
Então, o que você está testando basicamente é que um leão comerá como planejado e que um tigre comerá como planejado. É importante testar os dois, porque nem sempre é verdade que os dois comem exatamente da mesma maneira; testando os dois, você garante que aquele que não deseja alterar não. Como esses dois são os HerbivoreEaters definidos, pelo menos até você adicionar Cheetah, você também testou que todos os HerbivoreEaters comerão como pretendido. Seu teste abrange completamente e exercita adequadamente o código (desde que você também faça todas as afirmações esperadas sobre o que deve resultar de um HerbivoreEater comendo um Herbivore).
fonte