Unit Test zum Testen der Erstellung eines Domänenobjekts

11

Ich habe einen Unit Test, der so aussieht:

[Test]
public void Should_create_person()
{
     Assert.DoesNotThrow(() => new Person(Guid.NewGuid(), new DateTime(1972, 01, 01));
}

Ich behaupte, dass hier ein Personenobjekt erstellt wird, dh dass die Validierung nicht fehlschlägt. Wenn der Guid beispielsweise null ist oder das Geburtsdatum vor dem 01.01.1900 liegt, schlägt die Validierung fehl und eine Ausnahme wird ausgelöst (was bedeutet, dass der Test fehlschlägt).

Der Konstruktor sieht folgendermaßen aus:

public Person(Id id, DateTime dateOfBirth) :
        base(id)
    {
        if (dateOfBirth == null)
            throw new ArgumentNullException("Date of Birth");
        elseif (dateOfBith < new DateTime(1900,01,01)
            throw new ArgumentException("Date of Birth");
        DateOfBirth = dateOfBirth;
    }

Ist das eine gute Idee für einen Test?

Hinweis : Ich verfolge einen klassizistischen Ansatz zum Unit-Testen des Domänenmodells, wenn dies von Bedeutung ist.

w0051977
quelle
Verfügt der Konstruktor über eine Logik, die es wert ist, nach der Initialisierung bestätigt zu werden?
Laiv
2
Niemals Konstruktoren testen !!! Der Bau sollte einfach sein. Erwarten Sie Fehler in Guid.NewGuid () oder im Konstruktor von DateTime?
ivenxu
@Laiv, siehe das Update zur Frage.
w0051977
1
Es ist nichts wert, einen Test als den von Ihnen freigegebenen zu implementieren. Allerdings würde ich auch das Gegenteil testen. Ich würde den Fall testen, in dem das Geburtsdatum einen Fehler verursacht. Das ist die Invariante der Klasse, die Sie unter Kontrolle und Test haben möchten.
Laiv
3
Der Test sieht gut aus, abgesehen von einer Sache: dem Namen. Should_create_person? Was soll eine Person schaffen? Geben Sie ihm einen aussagekräftigen Namen, wie Creating_person_with_valid_data_succeeds.
David Arno

Antworten:

18

Dies ist ein gültiger Test (obwohl ziemlich übereifrig) und ich mache ihn manchmal, um die Konstruktorlogik zu testen. Wie Laiv in den Kommentaren erwähnt hat, sollten Sie sich jedoch fragen, warum.

Wenn Ihr Konstruktor so aussieht:

public Person(Guid guid, DateTime dob)
{
  this.Guid = guid;
  this.Dob = dob;
}

Ist es sinnvoll zu testen, ob es wirft? Ob die Parameter richtig zugewiesen sind, kann ich verstehen, aber Ihr Test ist ziemlich übertrieben.

Wenn Ihr Test jedoch ungefähr so ​​funktioniert:

public Person(Guid guid, DateTime dob)
{
  if(guid == default(Guid)) throw new ArgumentException("Guid is invalid");
  if(dob == default(DateTime)) throw new ArgumentException("Dob is invalid");

  this.Guid = guid;
  this.Dob = dob;
}

Dann wird Ihr Test relevanter (da Sie tatsächlich irgendwo im Code Ausnahmen auslösen).

Eine Sache würde ich sagen, im Allgemeinen ist es eine schlechte Praxis, viel Logik in Ihrem Konstruktor zu haben. Die grundlegende Validierung (wie die oben durchgeführten Null- / Standardprüfungen) ist in Ordnung. Aber wenn Sie eine Verbindung zu Datenbanken herstellen und Daten von jemandem laden, riecht Code dort wirklich ...

Wenn Ihr Konstruktor es wert ist, getestet zu werden (weil viel Logik vorhanden ist), stimmt aus diesem Grund möglicherweise etwas anderes nicht.

Sie werden mit ziemlicher Sicherheit andere Tests haben, die diese Klasse in Geschäftslogikebenen abdecken. Konstruktoren und Variablenzuweisungen werden mit ziemlicher Sicherheit eine vollständige Abdeckung durch diese Tests erhalten. Daher ist es möglicherweise sinnlos, spezifische Tests speziell für den Konstruktor hinzuzufügen. Nichts ist jedoch schwarzweiß und ich hätte nichts gegen diese Tests, wenn ich sie mit Code überprüfen würde - aber ich würde fragen, ob sie über die Tests an anderer Stelle in Ihrer Lösung hinaus einen Mehrwert bieten.

In Ihrem Beispiel:

public Person(Id id, DateTime dateOfBirth) :
        base(id)
    {
        if (dateOfBirth == null)
            throw new ArgumentNullException("Date of Birth");
        elseif (dateOfBith < new DateTime(1900,01,01)
            throw new ArgumentException("Date of Birth");
        DateOfBirth = dateOfBirth;
    }

Sie führen nicht nur eine Validierung durch, sondern rufen auch einen Basiskonstruktor auf. Für mich ist dies ein weiterer Grund für diese Tests, da die Konstruktor- / Validierungslogik jetzt auf zwei Klassen aufgeteilt ist, was die Sichtbarkeit verringert und das Risiko unerwarteter Änderungen erhöht.

TLDR

Diese Tests haben einen gewissen Wert, die Validierungs- / Zuweisungslogik wird jedoch wahrscheinlich von anderen Tests in Ihrer Lösung abgedeckt. Wenn diese Konstruktoren viel Logik enthalten, die erhebliche Tests erfordert, deutet dies darauf hin, dass dort ein unangenehmer Codegeruch lauert.

Liath
quelle
@ Laith, Bitte sehen Sie das Update zu meiner Frage
w0051977
Ich stelle fest, dass Sie in Ihrem Beispiel einen Basiskonstruktor aufrufen. Meiner Meinung nach erhöht dies den Wert Ihres Tests. Die Konstruktorlogik ist jetzt auf zwei Klassen aufgeteilt und birgt daher ein etwas höheres Änderungsrisiko, sodass mehr Grund zum Testen besteht.
Liath
"Wenn Ihr Test jedoch so etwas macht:" <Meinen Sie nicht "wenn Ihr Konstruktor so etwas macht" ?
Kodos Johnson
"Diese Tests haben einen gewissen Wert" - interessanterweise zeigt der Wert, dass wir diesen Test überflüssig machen könnten, indem wir eine neue Klasse verwenden, um den dob der Person darzustellen (z. B. PersonBirthdate), der die Validierung des Geburtsdatums durchführt. Ebenso könnte die GuidPrüfung für die IdKlasse implementiert werden . Dies bedeutet, dass Sie diese Validierungslogik wirklich nicht mehr im PersonKonstruktor haben müssen, da es nicht möglich ist, eine mit ungültigen Daten zu konstruieren - außer für nullrefs. Natürlich müssen Sie Tests für die anderen beiden Klassen schreiben :)
Stephen Byrne
12

Hier schon eine gute Antwort, aber ich denke, eine weitere Sache ist erwähnenswert.

Wenn man TDD "by the book" macht, muss man zuerst einen Test schreiben, der den Konstruktor aufruft, noch bevor der Konstruktor implementiert wird. Dieser Test könnte tatsächlich so aussehen wie der von Ihnen vorgestellte, selbst wenn die Implementierung des Konstruktors keine Validierungslogik enthält.

Beachten Sie auch, dass man für TDD zuerst einen anderen Test wie schreiben sollte

  Assert.Throws<ArgumentException>(() => new Person(Guid.NewGuid(), 
        new DateTime(1572, 01, 01));

bevor Sie die Prüfung für DateTime(1900,01,01)zum Konstruktor hinzufügen .

Im TDD-Kontext ist der gezeigte Test durchaus sinnvoll.

Doc Brown
quelle
Schöner Winkel, an den ich nicht gedacht hatte!
Liath
1
Dies zeigt mir, warum eine so starre Form von TDD Zeitverschwendung ist: Der Test sollte nach dem Schreiben des Codes einen Wert haben , oder Sie schreiben jede Codezeile nur zweimal, einmal als Behauptung und einmal als Code. Ich würde argumentieren, dass der Konstruktor selbst keine Logik ist, die getestet werden muss. Die Geschäftsregel "Menschen, die vor 1900 geboren wurden, dürfen nicht darstellbar sein" ist testbar, und im Konstruktor wird diese Regel gerade implementiert. Wann würde der Test eines leeren Konstruktors jemals einen Mehrwert für das Projekt schaffen?
IMSoP
Ist es wirklich tdd nach dem Buch? Ich würde eine Instanz erstellen und ihre Methode sofort in einem Code aufrufen. Dann würde ich einen Test für diese Methode schreiben, und auf diese Weise müsste ich auch eine Instanz für diese Methode erstellen, damit sowohl der Konstruktor als auch die Methode in diesem Test behandelt werden. Es sei denn, im Konstruktor gibt es eine Logik, aber dieser Teil wird von Liath abgedeckt.
Rafał Łużyński
@ RafałŁużyński: Bei TDD "by the book" geht es darum, zuerst Tests zu schreiben . Es bedeutet eigentlich, immer zuerst einen fehlgeschlagenen Test zu schreiben (das Nicht-Kompilieren zählt ebenfalls als Fehler). Sie schreiben also zuerst einen Test, der den Konstruktor aufruft, auch wenn kein Konstruktor vorhanden ist . Dann versuchen Sie zu kompilieren (was fehlschlägt), dann implementieren Sie einen leeren Konstruktor, kompilieren, führen den Test aus, result = green. Dann schreiben Sie den ersten fehlgeschlagenen Test und führen ihn aus - result = red, dann fügen Sie die Funktionalität hinzu, um den Test wieder "grün" zu machen, und so weiter.
Doc Brown
Natürlich. Ich wollte nicht zuerst die Implementierung schreiben und dann testen. Ich schreibe einfach "Verwendung" dieses Codes in einer höheren Ebene, teste dann auf diesen Code und implementiere ihn dann. Ich mache normalerweise "Outside TDD".
Rafał Łużyński