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?
quelle
Antworten:
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.
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.
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
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.
quelle
setup()
Methode einfügen - nicht den Test selbst.approve()
irgendwie spröde, aber abhängig vonsetApproved(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.push()
undpop()
-Methoden unabhängig voneinander zu testen ?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:
quelle
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.
quelle
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. :-)
quelle
Du sagst
und
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).
quelle