In vielen Fällen habe ich möglicherweise eine vorhandene Klasse mit einem gewissen Verhalten:
class Lion
{
public void Eat(Herbivore herbivore) { ... }
}
... und ich habe einen Unit Test ...
[TestMethod]
public void Lion_can_eat_herbivore()
{
var herbivore = buildHerbivoreForEating();
var test = BuildLionForTest();
test.Eat(herbivore);
Assert.IsEaten(herbivore);
}
Was jetzt passiert, ist, dass ich eine Tiger-Klasse mit einem identischen Verhalten wie das des Löwen erstellen muss:
class Tiger
{
public void Eat(Herbivore herbivore) { ... }
}
... und da ich das gleiche Verhalten möchte, muss ich den gleichen Test ausführen, mache ich so etwas:
interface IHerbivoreEater
{
void Eat(Herbivore herbivore);
}
... und ich überarbeite meinen 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);
}
... und dann füge ich einen weiteren Test für meine neue Tiger
Klasse hinzu:
[TestMethod]
public void Tiger_can_eat_herbivore()
{
IHerbivoreEater_can_eat_herbivore(BuildTigerForTest);
}
... und dann überarbeite ich meine Lion
und Tiger
Klassen (normalerweise durch Vererbung, manchmal aber auch durch Komposition):
class Lion : HerbivoreEater { }
class Tiger : HerbivoreEater { }
abstract class HerbivoreEater : IHerbivoreEater
{
public void Eat(Herbivore herbivore) { ... }
}
... und alles ist gut. Da sich die Funktionalität jetzt in der HerbivoreEater
Klasse befindet, scheint es nun etwas falsch zu sein, Tests für jedes dieser Verhaltensweisen in jeder Unterklasse durchzuführen. Es sind jedoch die Unterklassen, die tatsächlich verwendet werden, und es ist nur ein Implementierungsdetail, dass sie zufällig überlappende Verhaltensweisen aufweisen ( Lions
und Tigers
beispielsweise völlig unterschiedliche Endverwendungen haben können).
Es scheint überflüssig, denselben Code mehrmals zu testen, aber es gibt Fälle, in denen die Unterklasse die Funktionalität der Basisklasse überschreiben kann und tut (ja, es könnte den LSP verletzen, aber seien wir ehrlich, es IHerbivoreEater
ist nur eine praktische Testschnittstelle - es ist ist für den Endbenutzer möglicherweise nicht wichtig). Ich denke, diese Tests haben einen gewissen Wert.
Was machen andere Leute in dieser Situation? Verschieben Sie Ihren Test einfach in die Basisklasse oder testen Sie alle Unterklassen auf das erwartete Verhalten?
EDIT :
Basierend auf der Antwort von @pdr denke ich, dass wir dies berücksichtigen sollten: Das IHerbivoreEater
ist nur ein Methodensignaturvertrag; Es gibt kein Verhalten an. Zum Beispiel:
[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);
}
quelle
Animal
Klasse haben, die enthältEat
? Alle Tiere fressen, und daher könnte die KlasseTiger
undLion
vom Tier erben.Eat
sollten alle Unterklassen dasselbeEat
Verhalten aufweisen , wenn Sie das Verhalten in die Basisklasse einfügen . Ich spreche jedoch von zwei relativ unabhängigen Klassen, die zufällig ein Verhalten teilen. Betrachten Sie zum Beispiel dasFly
Verhalten vonBrick
undPerson
das, wie wir annehmen können, ein ähnliches Flugverhalten aufweist, aber es ist nicht unbedingt sinnvoll, sie von einer gemeinsamen Basisklasse abzuleiten.Antworten:
Das ist großartig, weil es zeigt, wie Tests wirklich die Art und Weise beeinflussen, wie Sie über Design denken. Sie spüren Probleme im Design und stellen die richtigen Fragen.
Es gibt zwei Möglichkeiten, dies zu betrachten.
IHerbivoreEater ist ein Vertrag. Alle IHerbivoreEater müssen über eine Eat-Methode verfügen, die einen Pflanzenfresser akzeptiert. Nun ist es Ihren Tests egal, wie es gegessen wird; Ihr Löwe könnte mit den Hüften beginnen und Tiger könnte an der Kehle beginnen. Alles, was Ihren Test interessiert, ist, dass der Pflanzenfresser gegessen wird, nachdem er Eat genannt hat.
Auf der anderen Seite sagen Sie unter anderem, dass alle IHerbivoreEater den Pflanzenfresser genauso essen (daher die Basisklasse). In diesem Fall macht es keinen Sinn, überhaupt einen IHerbivoreEater-Vertrag zu haben. Es bietet nichts. Sie können auch einfach von HerbivoreEater erben.
Oder vielleicht Löwe und Tiger komplett abschaffen.
Wenn sich Löwe und Tiger jedoch in jeder Hinsicht von ihren Essgewohnheiten unterscheiden, müssen Sie sich fragen, ob Sie auf Probleme mit einem komplexen Erbbaum stoßen werden. Was ist, wenn Sie auch beide Klassen von Feline oder nur die Lion-Klasse von KingOfItsDomain (vielleicht zusammen mit Shark) ableiten möchten? Hier kommt LSP wirklich ins Spiel.
Ich würde vorschlagen, dass allgemeiner Code besser gekapselt ist.
Gleiches gilt für Tiger.
Nun, hier ist eine schöne Sache, die sich entwickelt (schön, weil ich es nicht beabsichtigt habe). Wenn Sie nur diesen privaten Konstruktor für den Test verfügbar machen, können Sie eine gefälschte IHerbivoreEatingStrategy übergeben und einfach testen, ob die Nachricht ordnungsgemäß an das gekapselte Objekt weitergeleitet wird.
Und Ihr komplexer Test, über den Sie sich zuerst Sorgen gemacht haben, muss nur die StandardHerbivoreEatingStrategy testen. Eine Klasse, eine Reihe von Tests, kein doppelter Code, über den man sich Sorgen machen muss.
Und wenn Sie Tigers später mitteilen möchten, dass sie ihre Pflanzenfresser anders essen sollen, muss sich keiner dieser Tests ändern. Sie erstellen einfach eine neue HerbivoreEatingStrategy und testen diese. Die Verkabelung wird auf Integrationstest-Ebene getestet.
quelle
IHerbivoreEater
ist ein Vertrag, aber nur insoweit, als ich ihn zum Testen brauche. Es scheint mir, dass dies ein Fall ist, in dem das Tippen von Enten wirklich helfen würde. Ich möchte sie jetzt nur noch an dieselbe Testlogik senden. Ich denke nicht, dass die Schnittstelle das Verhalten versprechen sollte. Die Tests sollten das tun.Umgekehrt fragen Sie, ob es angemessen ist, White-Box-Wissen zu verwenden, um einige Tests wegzulassen. Aus einer Black-Box-Perspektive
Lion
undTiger
sind verschiedene Klassen. Jemand, der mit dem Code nicht vertraut ist, würde ihn testen, aber Sie mit tiefem Implementierungswissen wissen, dass Sie mit dem einfachen Testen eines Tieres davonkommen können.Ein Grund für die Entwicklung von Komponententests besteht darin, dass Sie später eine Umgestaltung vornehmen können, aber dieselbe Black-Box-Schnittstelle beibehalten . Die Unit-Tests helfen Ihnen dabei, sicherzustellen, dass Ihre Klassen ihren Vertrag mit Kunden weiterhin erfüllen, oder zwingen Sie zumindest dazu, zu erkennen und sorgfältig zu überlegen, wie sich der Vertrag ändern könnte. Sie selbst erkennen dies
Lion
oder könnenTiger
esEat
zu einem späteren Zeitpunkt überschreiben . Wenn dies aus der Ferne möglich ist, kann ein einfacher Unit-Test durchgeführt werden, den jedes von Ihnen unterstützte Tier auf folgende Weise fressen kann:sollte sehr einfach zu machen sein und ausreichen und wird sicherstellen, dass Sie erkennen können, wenn Objekte ihren Vertrag nicht erfüllen.
quelle
Du machst es richtig. Stellen Sie sich einen Komponententest als Test für das Verhalten einer einzelnen Verwendung Ihres neuen Codes vor. Dies ist der gleiche Aufruf, den Sie vom Produktionscode aus ausführen würden.
In dieser Situation haben Sie völlig Recht. Ein Benutzer eines Löwen oder Tigers wird sich (zumindest nicht) nicht darum kümmern müssen, dass sie beide HerbivoreEaters sind und dass der Code, der tatsächlich für die Methode ausgeführt wird, beiden in der Basisklasse gemeinsam ist. Ebenso ist es einem Benutzer eines HerbivoreEater-Abstracts (bereitgestellt vom konkreten Löwen oder Tiger) egal, welches es hat. Was sie interessiert ist, dass ihr Löwe, Tiger oder eine unbekannte konkrete Implementierung von HerbivoreEater einen Pflanzenfresser korrekt frisst ().
Was Sie also im Grunde testen, ist, dass ein Löwe wie beabsichtigt frisst und dass ein Tiger wie beabsichtigt frisst. Es ist wichtig, beide zu testen, da es möglicherweise nicht immer wahr ist, dass beide genau gleich essen. Indem Sie beide testen, stellen Sie sicher, dass derjenige, den Sie nicht ändern wollten, dies nicht tat. Da dies beide definierten HerbivoreEaters sind, haben Sie zumindest bis zum Hinzufügen von Cheetah auch getestet, dass alle HerbivoreEaters wie beabsichtigt essen. Ihre Tests decken den Code vollständig ab und üben ihn angemessen aus (vorausgesetzt, Sie machen auch alle erwarteten Aussagen darüber, was daraus resultieren sollte, wenn ein Pflanzenfresser einen Pflanzenfresser isst).
quelle