In molti casi potrei avere una classe esistente con un certo 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, ciò che succede è che devo creare una classe Tiger con un comportamento identico a quello del Lion:
class Tiger
{
public void Eat(Herbivore herbivore) { ... }
}
... e poiché voglio lo stesso comportamento, devo eseguire lo stesso test, faccio qualcosa del genere:
interface IHerbivoreEater
{
void Eat(Herbivore herbivore);
}
... e rifatto 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 Tigerclasse:
[TestMethod]
public void Tiger_can_eat_herbivore()
{
IHerbivoreEater_can_eat_herbivore(BuildTigerForTest);
}
... e poi refactoring my Lione Tigerclass (di solito per eredità, 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 HerbivoreEaterclasse, ora sembra che ci sia qualcosa di sbagliato nell'avere test per ciascuno di questi comportamenti su ogni sottoclasse. Eppure sono le sottoclassi che vengono effettivamente consumate ed è solo un dettaglio dell'implementazione che condividono comportamenti sovrapposti ( Lionse Tigerspossono avere usi finali totalmente diversi, per esempio).
Sembra ridondante testare lo stesso codice più volte, ma ci sono casi in cui la sottoclasse può e sostituisce la funzionalità della classe base (sì, potrebbe violare l'LSP, ma ammettiamolo, 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? Sposta semplicemente il test nella classe base o verifichi tutte le sottoclassi per il comportamento previsto?
MODIFICA :
Sulla base della risposta di @pdr penso che dovremmo considerare questo: si IHerbivoreEatertratta solo di un contratto di firma del metodo; non specifica il comportamento. Per 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);
}
Eatcomportamento nella classe base, tutte le sottoclassi dovrebbero mostrare lo stesso Eatcomportamento. Tuttavia, sto parlando di 2 classi relativamente non correlate che condividono un comportamento. Si consideri, ad esempio, il Flycomportamento Bricke Personche, possiamo supporre, esibiscono un comportamento di volo simile, ma non ha necessariamente senso che derivino da una classe base comune.
Animalclasse che contieneEat? Tutti gli animali mangiano, e quindi la classeTigereLionpotrebbe ereditare dall'animale.