In molti casi potrei avere una classe esistente con qualche comportamento:
class Lion
{
public void Eat(Herbivore herbivore) { ... }
}
... e ho un test unitario ...
[TestMethod]
public void Lion_can_eat_herbivore()
{
var herbivore = buildHerbivoreForEating();
var test = BuildLionForTest();
test.Eat(herbivore);
Assert.IsEaten(herbivore);
}
Ora, quello che succede è che ho bisogno di creare una classe Tiger con un comportamento identico a quello del Lion:
class Tiger
{
public void Eat(Herbivore herbivore) { ... }
}
... e dal momento che voglio lo stesso comportamento, ho bisogno di eseguire lo stesso test, faccio qualcosa del genere:
interface IHerbivoreEater
{
void Eat(Herbivore herbivore);
}
... e refripto il mio test:
[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 poi aggiungo un altro test per la mia nuova Tiger
class:
[TestMethod]
public void Tiger_can_eat_herbivore()
{
IHerbivoreEater_can_eat_herbivore(BuildTigerForTest);
}
... e poi refacifico le mie classi Lion
e Tiger
(di solito per ereditarietà, ma a volte per composizione):
class Lion : HerbivoreEater { }
class Tiger : HerbivoreEater { }
abstract class HerbivoreEater : IHerbivoreEater
{
public void Eat(Herbivore herbivore) { ... }
}
... e tutto va bene. Tuttavia, poiché la funzionalità è ora nella classe HerbivoreEater
, ora sembra che ci sia qualcosa di sbagliato nell'avere test per ciascuno di questi comportamenti su ciascuna sottoclasse. Eppure sono le sottoclassi che vengono effettivamente consumate, ed è solo un dettaglio di implementazione che condividono comportamenti sovrapposti ( Lions
e Tigers
potrebbero avere usi finali completamente diversi, per esempio).
Sembra ridondante testare lo stesso codice più volte, ma ci sono casi in cui la sottoclasse può e sovrascrive la funzionalità della classe base (sì, potrebbe violare l'LSP, ma lasciamolo affrontare, IHerbivoreEater
è solo una comoda interfaccia di test - potrebbe non essere importante per l'utente finale). Quindi questi test hanno un certo valore, penso.
Cosa fanno gli altri in questa situazione? Spostate semplicemente il test sulla classe base o testate tutte le sottoclassi per il comportamento previsto?
Modifica :
Sulla base della risposta di @pdr, dovremmo considerare questo: il IHerbivoreEater
è solo un contratto di firma del metodo; non specifica il comportamento. Ad esempio:
[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);
}