Unit Testing in einer "no setter" Welt

23

Ich betrachte mich nicht als DDD-Experte, sondern versuche als Lösungsarchitekt, wenn immer möglich, Best Practices anzuwenden. Ich weiß, dass es eine Menge Diskussionen um die Vor- und Nachteile des No-Setter-Stils in DDD gibt, und ich kann beide Seiten des Arguments sehen. Mein Problem ist, dass ich in einem Team mit einer großen Vielfalt an Fähigkeiten, Kenntnissen und Erfahrungen arbeite, was bedeutet, dass ich nicht darauf vertrauen kann, dass jeder Entwickler die Dinge "richtig" macht. Wenn zum Beispiel unsere Domänenobjekte so konzipiert sind, dass Änderungen am internen Status des Objekts von einer Methode durchgeführt werden, die jedoch öffentliche Eigenschaftssetzer bereitstellt, wird die Eigenschaft unvermeidlich festgelegt, anstatt die Methode aufzurufen. Verwenden Sie dieses Beispiel:

public class MyClass
{
    public Boolean IsPublished
    {
        get { return PublishDate != null; }
    }

    public DateTime? PublishDate { get; set; }

    public void Publish()
    {
        if (IsPublished)
            throw new InvalidOperationException("Already published.");

        PublishDate = DateTime.Today;

        Raise(new PublishedEvent());
    }
}

Meine Lösung bestand darin, Eigenschaftssetzer privat zu machen, was möglich ist, da der ORM, den wir zum Hydratisieren der Objekte verwenden, Reflexion verwendet, um auf private Setzer zugreifen zu können. Dies stellt jedoch ein Problem dar, wenn versucht wird, Komponententests zu schreiben. Wenn ich zum Beispiel einen Komponententest schreiben möchte, der bestätigt, dass das Objekt nicht erneut veröffentlicht werden kann, muss ich darauf hinweisen, dass es bereits veröffentlicht wurde. Ich kann dies sicherlich tun, indem ich Publish zweimal aufrufe, aber mein Test geht dann davon aus, dass Publish beim ersten Aufruf korrekt implementiert ist. Das scheint ein wenig zu stinken.

Lassen Sie uns das Szenario mit folgendem Code etwas realistischer gestalten:

public class Document
{
    public Document(String title)
    {
        if (String.IsNullOrWhiteSpace(title))
            throw new ArgumentException("title");

        Title = title;
    }

    public String ApprovedBy { get; private set; }
    public DateTime? ApprovedOn { get; private set; }
    public Boolean IsApproved { get; private set; }
    public Boolean IsPublished { get; private set; }
    public String PublishedBy { get; private set; }
    public DateTime? PublishedOn { get; private set; }
    public String Title { get; private set; }

    public void Approve(String by)
    {
        if (IsApproved)
            throw new InvalidOperationException("Already approved.");

        ApprovedBy = by;
        ApprovedOn = DateTime.Today;
        IsApproved = true;

        Raise(new ApprovedEvent(Title));
    }

    public void Publish(String by)
    {
        if (IsPublished)
            throw new InvalidOperationException("Already published.");

        if (!IsApproved)
            throw new InvalidOperationException("Cannot publish until approved.");

        PublishedBy = by;
        PublishedOn = DateTime.Today;
        IsPublished = true;

        Raise(new PublishedEvent(Title));
    }
}

Ich möchte Komponententests schreiben, die Folgendes bestätigen:

  • Ich kann nur veröffentlichen, wenn das Dokument genehmigt wurde
  • Ich kann ein Dokument nicht erneut veröffentlichen
  • Bei der Veröffentlichung werden die Werte PublishedBy und PublishedOn ordnungsgemäß festgelegt
  • Bei Veröffentlichung wird das PublishedEvent ausgelöst

Ohne Zugriff auf die Setter kann ich das Objekt nicht in den Status versetzen, der für die Durchführung der Tests erforderlich ist. Das Öffnen des Zugangs zu den Setzern verhindert den Zugang.

Wie haben Sie dieses Problem gelöst?

SonOfPirate
quelle
Je mehr ich darüber nachdenke, desto mehr denke ich, dass Ihr gesamtes Problem Methoden mit Nebenwirkungen hat. Oder vielmehr ein veränderliches unveränderliches Objekt. Sollten Sie in einer DDD-Welt nicht ein neues Dokumentobjekt von Approve und Publish zurückgeben, anstatt den internen Status dieses Objekts zu aktualisieren?
pdr
1
Kurze Frage, welches O / RM benutzt du? Ich bin ein großer Fan von EF, aber es reibt mich ein bisschen, Setter als geschützt zu deklarieren.
Michael Brown
Wir haben im Moment eine Mischung aufgrund der Entwicklung der freien Reichweite, mit der ich mich befasst habe. Einige ADO.NET-Anwendungen, die AutoMapper verwenden, um Daten aus einem DataReader zu hydrieren, einige Linq-to-SQL-Modelle (die als nächstes ersetzt werden sollen) ) und einige neue EF-Modelle.
SonOfPirate
Das zweimalige Aufrufen von Publish riecht überhaupt nicht und ist der richtige Weg, dies zu tun.
Piotr Perak

Antworten:

27

Ich kann das Objekt nicht in den für die Durchführung der Tests erforderlichen Zustand versetzen.

Wenn Sie das Objekt nicht in den Status versetzen können, der zum Ausführen eines Tests erforderlich ist, können Sie das Objekt im Produktionscode nicht in den Status versetzen, sodass dieser Status nicht getestet werden muss . Offensichtlich ist dies in Ihrem Fall nicht der Fall. Sie können Ihr Objekt in den erforderlichen Zustand versetzen. Rufen Sie einfach Approve auf.

  • Ich kann nur veröffentlichen, wenn das Dokument genehmigt wurde: Schreiben Sie einen Test, bei dem der Aufruf von "Publizieren" vor dem Aufruf von "Genehmigen" den richtigen Fehler verursacht, ohne den Objektstatus zu ändern.

    void testPublishBeforeApprove() {
        doc = new Document("Doc");
        AssertRaises(doc.publish, ..., NotApprovedException);
    }
    
  • Ich kann ein Dokument nicht erneut veröffentlichen: Schreiben Sie einen Test, der ein Objekt genehmigt, und rufen Sie dann "Publizieren" auf, sobald der Test erfolgreich war. Das zweite Mal führt jedoch zu dem richtigen Fehler, ohne den Objektstatus zu ändern.

    void testRePublish() {
        doc = new Document("Doc");
        doc.approve();
        doc.publish();
        AssertRaises(doc.publish, ..., RepublishException);
    }
    
  • Bei der Veröffentlichung werden die Werte PublishedBy und PublishedOn ordnungsgemäß festgelegt: Schreiben Sie einen Test, der von Aufrufen genehmigt wird, und rufen Sie Publish auf. Stellen Sie dabei sicher, dass sich der Objektstatus ordnungsgemäß ändert

    void testPublish() {
        doc = new Document("Doc");
        doc.approve();
        doc.publish();
        Assert(doc.PublishedBy, ...);
        ...
    }
    
  • Bei der Veröffentlichung wird das PublishedEvent ausgelöst: Stellen Sie eine Verbindung zum Ereignissystem her und setzen Sie ein Flag, um sicherzustellen, dass es aufgerufen wird

Sie müssen auch einen Test für die Genehmigung schreiben.

Mit anderen Worten, testen Sie nicht die Beziehung zwischen internen Feldern und IsPublished und IsApproved. Wenn Sie dies tun, ist Ihr Test ziemlich fragil, da das Ändern Ihres Felds das Ändern Ihres Testcodes bedeuten würde, sodass der Test ziemlich sinnlos wäre. Stattdessen sollten Sie die Beziehung zwischen Aufrufen öffentlicher Methoden auf diese Weise testen, auch wenn Sie die Felder ändern, die Sie nicht benötigen, um den Test zu ändern.

Lüge Ryan
quelle
Wenn Genehmigen unterbrochen wird, werden mehrere Tests unterbrochen. Sie testen keine Codeeinheit mehr, sondern die vollständige Implementierung.
pdr
Ich teile die Besorgnis von pdr, weshalb ich gezögert habe, in diese Richtung zu gehen. Ja, es scheint am saubersten zu sein, aber ich mag es nicht, wenn ein einzelner Test aus mehreren Gründen fehlschlägt.
SonOfPirate
4
Ich habe noch keinen Komponententest gesehen, der nur aus einem einzigen Grund scheitern könnte. Sie können auch die "Zustandsmanipulations" -Teile des Tests in eine setup()Methode einfügen - nicht den Test selbst.
Peter K.
12
Warum ist abhängig von approve()irgendwie spröde, aber abhängig von setApproved(true)irgendwie nicht? approve()ist eine legitime Abhängigkeit in den Tests, weil es eine Abhängigkeit in den Anforderungen ist. Wenn die Abhängigkeit nur in den Tests vorhanden wäre, wäre dies ein weiteres Problem.
Karl Bielefeldt
2
@pdr, wie würden Sie eine Stapelklasse testen? Würden Sie versuchen, die push()und pop()-Methoden unabhängig voneinander zu testen ?
Winston Ewert
2

Ein weiterer Ansatz besteht darin, einen Konstruktor für die Klasse zu erstellen, mit dem die internen Eigenschaften bei der Instanziierung festgelegt werden können:

 public Document(
  String approvedBy,
  DateTime? approvedOn,
  Boolean isApproved,
  Boolean isPublished,
  String publishedBy,
  DateTime? publishedOn,
  String title)
{
  ApprovedBy = approvedBy;
  ApprovedOn = approvedOn;
  IsApproved = isApproved;
  IsApproved = isApproved;
  PublishedBy = publishedBy;
  PublishedOn = publishedOn;
}
Peter K.
quelle
2
Dies skaliert überhaupt nicht gut. Mein Objekt könnte viel mehr Eigenschaften haben, von denen eine beliebige Anzahl zu einem bestimmten Zeitpunkt im Lebenszyklus des Objekts Werte aufweist oder nicht. Ich folge dem Prinzip, dass Konstruktoren Parameter für Eigenschaften enthalten, die erforderlich sind, damit sich das Objekt in einem gültigen Anfangszustand befindet, oder für Abhängigkeiten, die ein Objekt benötigt, um zu funktionieren. Der Zweck der Eigenschaften im Beispiel besteht darin, den aktuellen Status zu erfassen, während das Objekt bearbeitet wird. Einen Konstruktor mit jeder Eigenschaft oder Überladungen mit verschiedenen Kombinationen zu haben, ist ein riesiger Geruch und, wie gesagt, nicht skalierbar.
SonOfPirate
Verstanden. In Ihrem Beispiel wurden nicht viele weitere Eigenschaften erwähnt, und die Zahl im Beispiel ist "kurz davor", dies als einen gültigen Ansatz zu haben. Anscheinend sagt dies etwas über Ihr Design aus: Sie können Ihr Objekt beim Instanziieren nicht in einen gültigen Zustand versetzen . Das bedeutet, dass Sie es in einen gültigen Anfangszustand versetzen müssen und sie es für den Test in den richtigen Zustand bringen müssen. Dies impliziert, dass Lie Ryans Antwort der richtige Weg ist .
Peter K.
Auch wenn das Objekt eine Eigenschaft hat und diese niemals ändern wird, ist diese Lösung schlecht. Was hindert jemanden daran, diesen Konstruktor in der Produktion zu verwenden? Wie werden Sie diesen Konstruktor [TestOnly] markieren?
Piotr Perak
Warum ist es schlecht in der Produktion? (Wirklich, ich würde gerne wissen). Manchmal ist es notwendig, den genauen Zustand eines Objekts bei der Erstellung wiederherzustellen ... nicht nur ein einzelnes gültiges Anfangsobjekt.
Peter K.
1
Während dies hilft, das Objekt in einen gültigen Anfangszustand zu versetzen, erfordert das Testen des Verhaltens des Objekts im Verlauf seines Lebenszyklus, dass das Objekt von seinem Anfangszustand geändert wird. Mein OP hat mit dem Testen dieser zusätzlichen Zustände zu tun, wenn Sie nicht einfach Eigenschaften festlegen können, um den Zustand des Objekts zu ändern.
SonOfPirate
1

Eine Strategie besteht darin, dass Sie die Klasse (in diesem Fall Document) erben und Tests für die geerbte Klasse schreiben. Die geerbte Klasse ermöglicht das Festlegen des Objektstatus in Tests.

In C # könnte eine Strategie darin bestehen, Setter intern zu machen und dann Internals für das Testprojekt verfügbar zu machen.

Sie können auch die von Ihnen beschriebene Klassen-API verwenden ("Ich kann dies auf jeden Fall tun, indem Sie" Publish "zweimal aufrufen"). Dies würde bedeuten, den Objektstatus mithilfe der öffentlichen Dienste des Objekts festzulegen. Es scheint mir nicht zu stinkend. Im Fall Ihres Beispiels wäre dies wahrscheinlich die Art und Weise, wie ich es tun würde.

Simoraman
quelle
Ich dachte über eine mögliche Lösung nach, zögerte jedoch, meine Eigenschaften außer Kraft zu setzen oder die Setter als geschützt auszusetzen, da ich das Gefühl hatte, das Objekt zu öffnen und die Kapselung zu lösen. Ich denke, dass es sicher besser ist, die Eigenschaften geschützt zu machen, als öffentlich oder sogar intern / befreundet. Ich werde auf jeden Fall mehr über diesen Ansatz nachdenken. Es ist einfach und effektiv. Manchmal ist das der beste Ansatz. Wenn jemand anderer Meinung ist, fügen Sie bitte Kommentare mit Einzelheiten hinzu.
SonOfPirate
1

Um die Befehle und Abfragen , die die Domänenobjekte erhalten, in absoluter Isolation zu testen , versorge ich jeden Test mit einer Serialisierung des Objekts im erwarteten Zustand. Im Arrangierabschnitt des Tests wird das zu testende Objekt aus einer zuvor vorbereiteten Datei geladen. Zuerst habe ich mit binären Serialisierungen begonnen, aber es hat sich gezeigt, dass JSON viel einfacher zu verwalten ist. Dies hat sich bewährt, wenn die absolute Isolation in Tests den tatsächlichen Wert liefert.

Bearbeiten Sie nur eine Notiz, manchmal schlägt die JSON-Serialisierung fehl (wie bei zyklischen Objektgraphen, die übrigens ein Geruch sind). In solchen Situationen rette ich auf binäre Serialisierung. Es ist ein bisschen pragmatisch, funktioniert aber. :-)

Giacomo Tesio
quelle
Und wie bereiten Sie ein Objekt im erwarteten Zustand vor, wenn es keinen Setter gibt und Sie nicht dessen öffentliche Methoden aufrufen möchten, um es einzurichten?
Piotr Perak
Ich habe dafür ein kleines Tool geschrieben. Durch Reflektion wird eine Klasse geladen, die mit ihrem öffentlichen Konstruktor eine neue Entität erstellt (in der Regel nur mit dem Bezeichner) und ein Array von Action <TEntity> darauf aufruft. Nach jeder Operation wird ein Snapshot gespeichert (mit einem herkömmlichen Namen basierend auf dem Index der Aktion) und den Entitätsnamen). Das Tool wird bei jedem Refactoring des Entity-Codes manuell ausgeführt und die Snapshots werden vom DCVS nachverfolgt. Offensichtlich ruft jede Aktion ein öffentliches Kommando der Entität auf, aber dies geschieht aus den Testläufen heraus, die auf diese Weise wirklich Unit- Test sind.
Giacomo Tesio
Ich verstehe nicht, wie das etwas ändert. Wenn es immer noch öffentliche Methoden auf sut (System im Test) aufruft, ist es nicht anders, als nur diese Methoden im Test aufzurufen.
Piotr Perak
Nachdem die Schnappschüsse erstellt wurden, werden sie in Dateien gespeichert. Jeder Test hängt nicht von der Reihenfolge der Operationen ab, die zum Abrufen des Startzustands der Entität erforderlich sind, sondern vom Zustand selbst (geladen aus dem Snapshot). Die zu testende Methode selbst wird dann von Änderungen an den anderen Methoden isoliert.
Giacomo Tesio
Was passiert, wenn jemand die öffentliche Methode ändert, die zum Vorbereiten des serialisierten Status für Ihre Tests verwendet wurde, jedoch vergisst, das Tool zum erneuten Generieren des serialisierten Objekts auszuführen? Tests sind auch dann noch grün, wenn ein Fehler im Code vorliegt. Trotzdem sage ich, das ändert nichts. Sie führen weiterhin öffentliche Methoden aus, um die zu testenden Objekte einzurichten. Aber Sie führen sie lange vor den Tests aus.
Piotr Perak
-7

Du sagst

Versuchen Sie nach Möglichkeit, bewährte Methoden anzuwenden

und

Der ORM, den wir zum Hydratisieren der Objekte verwenden, verwendet Reflexion, um auf private Setter zugreifen zu können

und ich muss denken, dass die Verwendung von Reflektion zur Umgehung der Zugriffskontrollen in Ihren Klassen nicht das ist, was ich als "Best Practice" bezeichnen würde. Es wird auch schrecklich langsam sein.


Persönlich würde ich Ihr Unit-Test-Framework verschrotten und mich für etwas in der Klasse entscheiden - es scheint, dass Sie Tests schreiben, um die gesamte Klasse zu testen, was gut ist. In der Vergangenheit habe ich für einige knifflige Komponenten, die getestet werden mussten, die Asserts und den Setup-Code in die Klasse selbst eingebettet (früher war es ein gängiges Entwurfsmuster, in jeder Klasse eine test () -Methode zu haben), sodass Sie einen Client erstellen das instanziiert einfach ein Objekt und ruft die Testmethode auf, die sich einrichten kann, wie Sie möchten, ohne böse zu sein wie Reflection Hacks.

Wenn Sie sich Sorgen über Code machen, packen Sie die Testmethoden einfach in #ifdefs, um sie nur im Debug-Code verfügbar zu machen (wahrscheinlich eine bewährte Methode selbst).

gbjbaanb
quelle
4
-1: Das Verschrotten Ihres Test-Frameworks und die Rückkehr zu Testmethoden innerhalb der Klasse würde in die dunklen Zeiten des Unit-Tests zurückreichen.
Robert Johnson
9
Nein -1 von mir, aber das Einbeziehen von Testcode in die Produktion ist im Allgemeinen eine schlechte Sache (TM) .
Peter K.
Was macht das OP noch? Bleib beim Schrauben mit Privatleuten ?! Es ist so, als würde man wählen, welches Gift man trinken möchte. Mein Vorschlag an das OP war, den Komponententest in den Debug-Code zu setzen, nicht in die Produktion. Meiner Erfahrung nach bedeutet Unit-Tests in einem anderen Projekt, dass das Projekt sowieso eng mit dem Original verbunden ist. Von einem Entwickler-PoV gibt es also kaum einen Unterschied.
gbjbaanb