Ist das Erstellen von Objekten mit Null-Parametern in Komponententests in Ordnung?

37

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. SpeedLimitbraucht a Carmit a Motorum 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?

R. Schmitz
quelle
24
Etwas abseits des Themas, sollte aber Motorwohl gar kein haben speed. Es sollte ein throttleund ein torquebasierend auf dem aktuellen rpmund berechnen throttle. Es ist die Aufgabe des Autos, a zu verwenden Transmission, um dies in eine aktuelle Geschwindigkeit zu integrieren, und um dies in ein rpmSignal zurück zum Motor... zu verwandeln. Aber ich denke, Sie waren sowieso nicht für den Realismus dabei, oder?
CMASTER
17
Der Codegeruch ist, dass Ihr Konstruktor null annimmt, ohne sich überhaupt zu beschweren. Wenn Sie denken, dass dies akzeptabel ist (möglicherweise haben Sie Gründe dafür): Probieren Sie es aus!
Tommy
4
Betrachten Sie das Null-Objekt-Muster . Es vereinfacht häufig den Code und macht ihn gleichzeitig NPE-sicher.
Bakuriu
17
Herzlichen Glückwunsch : Sie haben getestet, dass mit einem nullRadio 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 ...
Matthieu M.
3
Nicht sicher
Owen

Antworten:

70

Im obigen Beispiel ist es sinnvoll, dass a Carohne a existieren kann CarRadio. In diesem Fall würde ich sagen, dass es nicht nur akzeptabel ist, CarRadiomanchmal eine Null zu übergeben , sondern auch, dass dies obligatorisch ist. Ihre Tests müssen sicherstellen, dass die Implementierung der CarKlasse robust ist und keine Nullzeigerausnahmen auslöst, wenn keine CarRadiovorhanden sind.

Nehmen wir jedoch ein anderes Beispiel - betrachten wir a SteeringWheel. Nehmen wir an, a Carmuss a haben SteeringWheel, aber der Geschwindigkeitstest kümmert sich nicht wirklich darum. In diesem Fall würde ich keine Null übergeben, SteeringWheelda dies die CarKlasse an Stellen drängt, an denen sie nicht vorgesehen ist. In diesem Fall ist es besser, eine Art zu erstellen, DefaultSteeringWheeldie (um die Metapher fortzusetzen) in einer geraden Linie eingeschlossen ist.

Pete
quelle
44
Und vergessen Sie nicht, einen CarSteeringWheel
Komponententest
13
Ich vermisse vielleicht etwas, aber wenn es möglich und sogar üblich ist , ein Carohne zu erstellen CarRadio, 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 ?
ein Ohrwurm
16
Selbstfahrende Autos haben möglicherweise kein Lenkrad. Ich sage es nur. ☺
Blackjack
1
@James_Parsons Ich habe vergessen hinzuzufügen, dass es sich um eine Klasse handelte, die keine Nullprüfungen aufwies, da sie nur intern verwendet wurde und immer Nicht-Null-Parameter erhielt. Andere Methoden von Carhätten ein NRE mit einer Null geworfen CarRadio, aber der entsprechende Code für SpeedLimitwürde nicht. Im Falle der Frage, wie sie jetzt gestellt wird, wären Sie richtig und ich würde das in der Tat tun.
R. Schmitz
2
@mtraceur Die Annahme, dass die Klasse, die die Konstruktion mit einem Nullargument zulässt, bedeutet, dass Null eine gültige Option ist, hat mir klar gemacht, dass ich dort wahrscheinlich Nullprüfungen durchführen sollte. Das eigentliche Problem, das ich dann hätte, wird durch viele gute, interessante und gut geschriebene Antworten abgedeckt, die ich nicht ruinieren möchte und die mir geholfen haben. Akzeptiere diese Antwort jetzt auch, weil ich es nicht mag, Dinge unvollendet zu lassen, und es ist das am besten bewertete (obwohl sie alle hilfreich waren).
R. Schmitz
23

Ist das Konstruieren von Objekten mit Null in Unit-Tests ein Codegeruch?

Ja. Beachten Sie die C2 Definition von Codegeruch : „a CodeSmell ist ein Hinweis darauf , dass etwas nicht in Ordnung sein könnte, keine Gewissheit.“

Der Test läuft gut. SpeedLimit braucht ein Auto mit einem Motor, um seine Sache zu erledigen. Es ist überhaupt nicht an einem CarRadio interessiert, deshalb habe ich null dafür angegeben.

Wenn Sie demonstrieren möchten , dass speedLimit.IsViolatedBy(car)dies nicht von dem CarRadioan 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 bemerken

  • Dies ist eine Verletzung der Kapselung (Sie lassen die Tatsache, dass sie CarRadionicht verwendet wird, in den Test auslaufen).
  • Sie haben mindestens einen Fall entdeckt, in dem Sie eine erstellen möchten, Carohne eine anzugebenCarRadio

Ich vermute sehr, dass der Code versucht, Ihnen mitzuteilen, dass Sie einen Konstruktor möchten, der aussieht

public Car(IMotor motor) {...}

und dann kann dieser Konstruktor entscheiden, was mit der Tatsache zu tun ist, dass es keine gibtCarRadio

public Car(IMotor motor) {
    this(null, motor);
}

oder

public Car(IMotor motor) {
    this( new ICarRadio() {...} , motor);
}

oder

public Car(IMotor motor) {
    this( new DefaultRadio(), motor);
}

etc.

Es geht darum, ob beim Testen von SpeedLimit Null an etwas anderes übergeben wird. Sie sehen, dass der Test "SpeedLimitTest" heißt und die einzige Zusicherung darin besteht, eine Methode von SpeedLimit zu überprüfen.

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.isViolatedByeine Annahme sein sollte Rolle Schnittstelle , die mit Auto Geräten , anstatt ein ganzes Auto zu benötigen.

interface IHaveSpeed {
    float speed();
}

class Car implements IHaveSpeed {...}

IHaveSpeedist ein wirklich mieser Name; Hoffentlich wird sich Ihre Domain-Sprache verbessern.

Mit dieser Schnittstelle SpeedLimitkönnte Ihr Test für viel einfacher sein

public void TestSpeedLimit()
{
    IHaveSpeed somethingTravelingQuickly = new IHaveSpeed {
        float speed() { return 10f; }
    }

    SpeedLimit speedLimit = new SpeedLimit();
    Assert.IsTrue(speedLimit.IsViolatedBy(somethingTravelingQuickly));
}
VoiceOfUnreason
quelle
In Ihrem letzten Beispiel müssen Sie vermutlich eine Mock-Klasse erstellen, die die IHaveSpeedSchnittstelle implementiert , da Sie (zumindest in c #) keine Schnittstelle selbst instanziieren können.
Ryan Searle
1
Das ist richtig - die effektive Rechtschreibung hängt davon ab, welchen Geschmack von Blub Sie für Ihre Tests verwenden.
VoiceOfUnreason
18

Ist das Konstruieren von Objekten mit Null in Unit-Tests ein Codegeruch?

Nein

Keineswegs. Wenn NULLes 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. Und NULL 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, NULList es ein guter Test , auch für den verbesserten Code zu bestehen. Ich möchte einen Test, um zu zeigen, was passiert, wenn ich NULLals Motor durchgehe. Das ist kein Codegeruch, das ist das Gegenteil von einem Codegeruch. Es testet.

nvoigt
quelle
Anscheinend schreiben Sie hier Testberichte Car. Ich sehe zwar nichts, was ich für falsch halten würde, aber es geht um das Testen SpeedLimit.
R. Schmitz
@ R.Schmitz Weiß nicht was du meinst. Ich habe die Frage zitiert, es geht um die Übergabe NULLan den Konstrukteur von Car.
Nvoigt
1
Es geht darum, ob beim TestenSpeedLimit Null an etwas anderes übergeben wird . Sie sehen, dass der Test "SpeedLimitTest" heißt und nur Asserteine Methode von überprüft SpeedLimit. Testen Car- zum Beispiel "Wenn Ihr Auto ohne Radio gebaut werden soll, sollten Sie einen Test schreiben" - würde in einem anderen Test ablaufen.
R. Schmitz
7

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:

class Car
{
   private readonly ICarRadio _carRadio;
   private readonly IMotor _motor;

   public Car(ICarRadio carRadio, IMotor motor)
   {
      if (carRadio == null)
         throw new ArgumentNullException(nameof(carRadio));

      if (motor == null)
         throw new ArgumentNullException(nameof(motor));

      _carRadio = carRadio;
      _motor = motor;
   }
}

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:

  1. Programmiere auf Interfaces anstatt auf Klassen.
  2. Verwenden Sie ein Mocking-Framework (wie Moq für C #), um verspottete Abhängigkeiten anstelle von Nullen einzufügen.

Für Ihr Beispiel hätten Sie also:

interface ICarRadio
{
   bool Tune(float frequency);
}

interface Motor
{
   bool SetSpeed(float speed);
}

...
var car = new Car(new Moq<ICarRadio>().Object, new Moq<IMotor>().Object);

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:

class DummyCarRadio : ICarRadio
{
   ...
}
class DummyMotor : IMotor
{
   ...
}

...
var car = new Car(new DummyCarRadio(), new DummyMotor())
Eternal21
quelle
3
Dies ist die Antwort, die ich geschrieben hätte
Liath
1
Ich würde sogar so weit gehen zu sagen, dass die Dummy-Objekte auch ihren Vertrag erfüllen müssen. Wenn IMotores eine getSpeed()Methode DummyMotorgäbe , müsste die nach einer erfolgreichen setSpeedauch eine geänderte Geschwindigkeit zurückgeben .
ComFreek
1

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.

Badeente
quelle
1

Kommen wir zu den ersten Prinzipien zurück.

Ein Komponententest testet einige Aspekte einer einzelnen Klasse.

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.

Robbie Dee
quelle
1
"Unit-Tests dienen zum Testen des Codes einer einzelnen Klasse." Seufz . Das ist nicht das, was Unit-Tests sind, und wenn Sie diese Richtlinie befolgen, werden Sie entweder in aufgeblähte Klassen geführt oder kontraproduktive, hinderliche Tests.
Ant P
3
"Einheit" als ein Euphemismus für "Klasse" zu behandeln, ist leider eine sehr verbreitete Denkweise, aber in Wirklichkeit ist alles, was dies tut, (a) "Garbage-In, Garbage-Out" -Tests anzuregen, die die Gültigkeit des gesamten Tests vollständig untergraben können Suites und (b) erzwingen Grenzen, die frei zu ändern sein sollten, was die Produktivität beeinträchtigen kann. Ich wünschte, mehr Menschen würden auf ihren Schmerz hören und dieses Vorurteil in Frage stellen.
Ant P
3
Keine solche Verwirrung. Testen Sie Verhaltenseinheiten, keine Codeeinheiten. Das Konzept eines Komponententests hat nichts zu bedeuten - außer in dem weit verbreiteten Frachtkult des Softwaretests -, dass eine Testeinheit an das eindeutig OOP-Konzept einer Klasse gekoppelt ist. Und das ist auch ein sehr schädliches Missverständnis.
Ant P
1
Ich weiß nicht genau, was Sie meinen - ich argumentiere genau das Gegenteil von dem, was Sie implizieren. Stummel / verspotten Sie harte Grenzen (wenn Sie unbedingt müssen) und testen Sie eine Verhaltenseinheit . Über die öffentliche API. Was diese API ist, hängt davon ab, was Sie erstellen, aber der Platzbedarf sollte minimal sein. Das Hinzufügen von Abstraktion, Polymorphismus oder Restrukturierungscode sollte nicht zum Fehlschlagen von Tests führen - "Einheit pro Klasse" widerspricht dem und untergräbt den Wert der Tests in erster Linie.
Ant P
1
RobbieDee - ich spreche über das genaue Gegenteil. Bedenken Sie, dass Sie derjenige sein könnten, der häufig falsche Vorstellungen hegt, überdenken Sie, was Sie als "Einheit" betrachten, und gehen Sie vielleicht etwas genauer auf das ein, worüber Kent Beck tatsächlich wirklich sprach, als er über TDD sprach. Was die meisten Leute jetzt TDD nennen, ist eine Frachtkult-Monstrosität. youtube.com/watch?v=EZ05e7EMOLM
Ant P
1

Wenn Sie sich Ihr Design ansehen, würde ich vorschlagen, dass sich a Caraus einer Reihe von zusammensetzt Features. Eine Eigenschaft vielleicht eine CarRadiooder EntertainmentSystemdie Dinge wie ein bestehen kann CarRadio, DvdPlayerusw.

Zumindest müsste man also eine Carmit einer Menge von konstruieren Features. EntertainmentSystemund CarRadiowäre Implementierer der Schnittstelle (Java, C # usw.) von, public [I|i]nterface Featureund dies wäre eine Instanz des zusammengesetzten Entwurfsmusters.

Daher würden Sie immer ein Carwith konstruieren Featuresund ein Unit-Test würde niemals ein Carwithout instanziieren Features. Sie können sich zwar überlegen, eine zu verspotten BasicTestCarFeatureSet, für die für Ihren grundlegendsten Test nur ein Mindestmaß an Funktionalität erforderlich ist.

Nur ein paar Gedanken.

John

johnd27
quelle