Ich habe angefangen, Unit-Tests für mein aktuelles Projekt zu schreiben. Ich habe allerdings keine wirkliche Erfahrung damit. Ich möchte es zuerst vollständig "bekommen", daher verwende ich derzeit weder mein IoC-Framework noch eine Spottbibliothek.
Ich habe mich gefragt, ob irgendetwas falsch daran ist, den Konstruktoren von Objekten in Komponententests Nullargumente zu liefern. Lassen Sie mich einen Beispielcode bereitstellen:
public class CarRadio
{...}
public class Motor
{
public void SetSpeed(float speed){...}
}
public class Car
{
public Car(CarRadio carRadio, Motor motor){...}
}
public class SpeedLimit
{
public bool IsViolatedBy(Car car){...}
}
Noch ein weiteres Car Code Example (TM), reduziert auf die für die Frage wichtigen Teile. Ich habe jetzt einen Test geschrieben, der ungefähr so aussieht:
public class SpeedLimitTest
{
public void TestSpeedLimit()
{
Motor motor = new Motor();
motor.SetSpeed(10f);
Car car = new Car(null, motor);
SpeedLimit speedLimit = new SpeedLimit();
Assert.IsTrue(speedLimit.IsViolatedBy(car));
}
}
Der Test läuft gut. SpeedLimit
braucht a Car
mit a Motor
um sein ding zu machen. Es ist überhaupt nicht daran interessiert CarRadio
, also habe ich null dafür angegeben.
Ich frage mich, ob ein Objekt, das die korrekte Funktionalität bietet, ohne vollständig konstruiert zu sein, eine Verletzung der SRP oder einen Codegeruch darstellt. Ich habe das quälende Gefühl, dass dies der Fall ist, aber ich speedLimit.IsViolatedBy(motor)
fühle mich auch nicht richtig - ein Tempolimit wird von einem Auto verletzt, nicht von einem Motor. Vielleicht brauche ich nur eine andere Perspektive für Unit-Tests im Vergleich zum Arbeitscode, weil die gesamte Absicht darin besteht, nur einen Teil des Ganzen zu testen.
Ist das Konstruieren von Objekten mit Null in Unit-Tests ein Codegeruch?
quelle
Motor
wohl gar kein habenspeed
. Es sollte einthrottle
und eintorque
basierend auf dem aktuellenrpm
und berechnenthrottle
. Es ist die Aufgabe des Autos, a zu verwendenTransmission
, um dies in eine aktuelle Geschwindigkeit zu integrieren, und um dies in einrpm
Signal zurück zumMotor
... zu verwandeln. Aber ich denke, Sie waren sowieso nicht für den Realismus dabei, oder?null
Radio das Tempolimit korrekt berechnet wird. Jetzt möchten Sie möglicherweise einen Test erstellen, um die Geschwindigkeitsbegrenzung mit einem Funkgerät zu überprüfen . Nur für den Fall, dass sich das Verhalten unterscheidet ...Antworten:
Im obigen Beispiel ist es sinnvoll, dass a
Car
ohne a existieren kannCarRadio
. In diesem Fall würde ich sagen, dass es nicht nur akzeptabel ist,CarRadio
manchmal eine Null zu übergeben , sondern auch, dass dies obligatorisch ist. Ihre Tests müssen sicherstellen, dass die Implementierung derCar
Klasse robust ist und keine Nullzeigerausnahmen auslöst, wenn keineCarRadio
vorhanden sind.Nehmen wir jedoch ein anderes Beispiel - betrachten wir a
SteeringWheel
. Nehmen wir an, aCar
muss a habenSteeringWheel
, aber der Geschwindigkeitstest kümmert sich nicht wirklich darum. In diesem Fall würde ich keine Null übergeben,SteeringWheel
da dies dieCar
Klasse an Stellen drängt, an denen sie nicht vorgesehen ist. In diesem Fall ist es besser, eine Art zu erstellen,DefaultSteeringWheel
die (um die Metapher fortzusetzen) in einer geraden Linie eingeschlossen ist.quelle
Car
SteeringWheel
Car
ohne zu erstellenCarRadio
, wäre es nicht sinnvoll, einen Konstruktor zu erstellen, für den keine Übergabe erforderlich ist und der sich überhaupt keine Gedanken über eine explizite Null machen muss ?Car
hätten ein NRE mit einer Null geworfenCarRadio
, aber der entsprechende Code fürSpeedLimit
würde nicht. Im Falle der Frage, wie sie jetzt gestellt wird, wären Sie richtig und ich würde das in der Tat tun.Ja. Beachten Sie die C2 Definition von Codegeruch : „a CodeSmell ist ein Hinweis darauf , dass etwas nicht in Ordnung sein könnte, keine Gewissheit.“
Wenn Sie demonstrieren möchten , dass
speedLimit.IsViolatedBy(car)
dies nicht von demCarRadio
an den Konstruktor übergebenen Argument abhängt , ist es für den zukünftigen Betreuer wahrscheinlich klarer, diese Einschränkung durch die Einführung eines Test-Double zu explizieren, das den Test nicht besteht, wenn es aufgerufen wird.Wenn Sie, wie es wahrscheinlicher ist, nur versuchen, sich die Arbeit zu ersparen
CarRadio
, von der Sie wissen, dass sie in diesem Fall nicht verwendet wird, sollten Sie das bemerkenCarRadio
nicht verwendet wird, in den Test auslaufen).Car
ohne eine anzugebenCarRadio
Ich vermute sehr, dass der Code versucht, Ihnen mitzuteilen, dass Sie einen Konstruktor möchten, der aussieht
und dann kann dieser Konstruktor entscheiden, was mit der Tatsache zu tun ist, dass es keine gibt
CarRadio
oder
oder
etc.
Das ist ein zweiter interessanter Geruch - um SpeedLimit zu testen, muss man ein ganzes Auto um ein Radio herum bauen, obwohl sich der SpeedLimit-Check wahrscheinlich nur um die Geschwindigkeit kümmert .
Dies könnte ein Hinweis darauf sein , dass
SpeedLimit.isViolatedBy
eine Annahme sein sollte Rolle Schnittstelle , die mit Auto Geräten , anstatt ein ganzes Auto zu benötigen.IHaveSpeed
ist ein wirklich mieser Name; Hoffentlich wird sich Ihre Domain-Sprache verbessern.Mit dieser Schnittstelle
SpeedLimit
könnte Ihr Test für viel einfacher seinquelle
IHaveSpeed
Schnittstelle implementiert , da Sie (zumindest in c #) keine Schnittstelle selbst instanziieren können.Nein
Keineswegs. Wenn
NULL
es sich hingegen um einen Wert handelt, der als Argument verfügbar ist (einige Sprachen können Konstrukte enthalten, die das Übergeben eines Nullzeigers nicht zulassen), sollten Sie ihn testen.Eine andere Frage ist, was passiert, wenn Sie passen
NULL
. Aber genau darum geht es bei einem Unit-Test: Sicherstellen, dass für jede mögliche Eingabe ein definiertes Ergebnis erfolgt. UndNULL
ist eine mögliche Eingabe, so dass Sie ein definiertes Ergebnis haben sollten und Sie sollten einen Test dafür haben.Also , wenn Ihr Auto ist angeblich ohne Radio sein konstruierbar, Sie sollten einen Test für das schreiben. Wenn es nicht und ist angeblich eine Ausnahme zu werfen, Sie sollten einen Test für das schreiben.
In jedem Fall sollten Sie einen Test für schreiben
NULL
. Es wäre ein Codegeruch, wenn Sie ihn weglassen würden. Das würde bedeuten, dass Sie einen nicht getesteten Code-Zweig haben, von dem Sie gerade angenommen haben, dass er auf magische Weise fehlerfrei ist.Bitte beachten Sie, dass ich mit den anderen Antworten einverstanden bin, dass Ihr getesteter Code verbessert werden könnte. Wenn der verbesserte Code jedoch Parameter enthält, die Zeiger sind,
NULL
ist es ein guter Test , auch für den verbesserten Code zu bestehen. Ich möchte einen Test, um zu zeigen, was passiert, wenn ichNULL
als Motor durchgehe. Das ist kein Codegeruch, das ist das Gegenteil von einem Codegeruch. Es testet.quelle
Car
. Ich sehe zwar nichts, was ich für falsch halten würde, aber es geht um das TestenSpeedLimit
.NULL
an den Konstrukteur vonCar
.SpeedLimit
Null an etwas anderes übergeben wird . Sie sehen, dass der Test "SpeedLimitTest" heißt und nurAssert
eine Methode von überprüftSpeedLimit
. TestenCar
- zum Beispiel "Wenn Ihr Auto ohne Radio gebaut werden soll, sollten Sie einen Test schreiben" - würde in einem anderen Test ablaufen.Ich persönlich würde zögern, Nullen zu injizieren. Tatsächlich ist eines der ersten Dinge, die ich tue, wenn ich Abhängigkeiten in einen Konstruktor injiziere, eine ArgumentNullException auszulösen, wenn diese Injektionen null sind. Auf diese Weise kann ich sie sicher in meiner Klasse verwenden, ohne dass Dutzende von Null-Checks herumliegen. Hier ist ein sehr verbreitetes Muster. Beachten Sie die Verwendung von readonly, um sicherzustellen, dass die im Konstruktor festgelegten Abhängigkeiten nicht auf null (oder irgendetwas anderes) gesetzt werden können:
Mit dem oben genannten könnte ich vielleicht einen oder zwei Unit-Tests durchführen, bei denen ich Nullen einspeise, um sicherzustellen, dass meine Nullenprüfungen wie erwartet funktionieren.
Dies ist jedoch kein Problem, sobald Sie zwei Dinge tun:
Für Ihr Beispiel hätten Sie also:
Weitere Details zur Verwendung von Moq finden Sie hier .
Wenn Sie es verstehen möchten, ohne sich vorher zu lustig zu machen, können Sie es natürlich trotzdem tun, solange Sie Schnittstellen verwenden. Erstellen Sie einfach einen Dummy-CarRadio und Motor wie folgt und injizieren Sie diese stattdessen:
quelle
IMotor
es einegetSpeed()
MethodeDummyMotor
gäbe , müsste die nach einer erfolgreichensetSpeed
auch eine geänderte Geschwindigkeit zurückgeben .Es ist nicht nur in Ordnung, es ist eine bekannte Technik zum Auflösen von Abhängigkeiten, um älteren Code in ein Test-Harness zu bekommen. (Siehe „Effektiv mit Legacy-Code arbeiten“ von Michael Feathers.)
Riecht es Gut...
Der Test riecht nicht. Der Test macht seinen Job. Es wird deklariert, dass dieses Objekt null sein kann und der getestete Code weiterhin korrekt funktioniert.
Der Geruch liegt in der Umsetzung. Indem Sie ein Konstruktorargument deklarieren, deklarieren Sie, dass diese Klasse dieses Ding benötigt , um richtig zu funktionieren. Offensichtlich ist das nicht wahr. Alles, was nicht stimmt, riecht ein bisschen.
quelle
Kommen wir zu den ersten Prinzipien zurück.
Es wird jedoch Konstruktoren oder Methoden geben, die andere Klassen als Parameter verwenden. Wie Sie damit umgehen, ist von Fall zu Fall unterschiedlich, aber die Einrichtung sollte minimal sein, da sie das Objektiv tangiert. Es kann Szenarien geben, in denen die Übergabe eines NULL-Parameters vollkommen gültig ist - beispielsweise ein Auto ohne Radio.
Ich gehe davon aus, dass wir zunächst nur die Rennlinie testen (um das Autothema fortzusetzen), aber Sie können auch NULL an andere Parameter übergeben, um zu überprüfen, ob etwaiger defensiver Code und Ausnahmebehandlungsmechanismen funktionieren erforderlich.
So weit, ist es gut. Was ist jedoch, wenn NULL kein gültiger Wert ist ? Nun, hier bist du in der düsteren Welt der Mocks, Stubs, Dummies und Fakes .
Wenn das alles ein bisschen zu viel zu bedeuten scheint, verwenden Sie bereits einen Dummy, indem Sie NULL im Car-Konstruktor übergeben, da dies ein Wert ist, der möglicherweise nicht verwendet wird.
Was ziemlich schnell klar werden sollte, ist, dass Sie potenziell Ihren Code mit viel NULL-Handling abschießen. Wenn Sie Klassenparameter durch Interfaces ersetzen, ebnen Sie auch den Weg für Mocking und DI usw., was die Handhabung erheblich vereinfacht.
quelle
Wenn Sie sich Ihr Design ansehen, würde ich vorschlagen, dass sich a
Car
aus einer Reihe von zusammensetztFeatures
. Eine Eigenschaft vielleicht eineCarRadio
oderEntertainmentSystem
die Dinge wie ein bestehen kannCarRadio
,DvdPlayer
usw.Zumindest müsste man also eine
Car
mit einer Menge von konstruierenFeatures
.EntertainmentSystem
undCarRadio
wäre Implementierer der Schnittstelle (Java, C # usw.) von,public [I|i]nterface Feature
und dies wäre eine Instanz des zusammengesetzten Entwurfsmusters.Daher würden Sie immer ein
Car
with konstruierenFeatures
und ein Unit-Test würde niemals einCar
without instanziierenFeatures
. Sie können sich zwar überlegen, eine zu verspottenBasicTestCarFeatureSet
, für die für Ihren grundlegendsten Test nur ein Mindestmaß an Funktionalität erforderlich ist.Nur ein paar Gedanken.
John
quelle