Meine Domain besteht aus vielen einfachen unveränderlichen Klassen wie dieser:
public class Person
{
public string FullName { get; }
public string NameAtBirth { get; }
public string TaxId { get; }
public PhoneNumber PhoneNumber { get; }
public Address Address { get; }
public Person(
string fullName,
string nameAtBirth,
string taxId,
PhoneNumber phoneNumber,
Address address)
{
if (fullName == null)
throw new ArgumentNullException(nameof(fullName));
if (nameAtBirth == null)
throw new ArgumentNullException(nameof(nameAtBirth));
if (taxId == null)
throw new ArgumentNullException(nameof(taxId));
if (phoneNumber == null)
throw new ArgumentNullException(nameof(phoneNumber));
if (address == null)
throw new ArgumentNullException(nameof(address));
FullName = fullName;
NameAtBirth = nameAtBirth;
TaxId = taxId;
PhoneNumber = phoneNumber;
Address = address;
}
}
Das Schreiben der Nullprüfungen und der Eigenschaftsinitialisierung wird bereits sehr mühsam. Derzeit schreibe ich Unit-Tests für jede dieser Klassen, um zu überprüfen, ob die Argumentvalidierung ordnungsgemäß funktioniert und alle Eigenschaften initialisiert wurden. Das fühlt sich nach extrem langweiliger Arbeit mit unangemessenem Nutzen an.
Die eigentliche Lösung wäre, dass C # Unveränderlichkeit und nicht nullbare Referenztypen nativ unterstützt. Aber was kann ich in der Zwischenzeit tun, um die Situation zu verbessern? Lohnt es sich, all diese Tests zu schreiben? Wäre es eine gute Idee, einen Codegenerator für solche Klassen zu schreiben, um zu vermeiden, dass Tests für jede einzelne geschrieben werden?
Hier ist, was ich jetzt basierend auf den Antworten habe.
Ich könnte die Nullprüfungen und die Eigenschaftsinitialisierung so vereinfachen:
FullName = fullName.ThrowIfNull(nameof(fullName));
NameAtBirth = nameAtBirth.ThrowIfNull(nameof(nameAtBirth));
TaxId = taxId.ThrowIfNull(nameof(taxId));
PhoneNumber = phoneNumber.ThrowIfNull(nameof(phoneNumber));
Address = address.ThrowIfNull(nameof(address));
Mit der folgenden Implementierung von Robert Harvey :
public static class ArgumentValidationExtensions
{
public static T ThrowIfNull<T>(this T o, string paramName) where T : class
{
if (o == null)
throw new ArgumentNullException(paramName);
return o;
}
}
Das Testen der Nullprüfungen ist mit dem GuardClauseAssertion
von einfach AutoFixture.Idioms
(danke für den Vorschlag, Esben Skov Pedersen ):
var fixture = new Fixture().Customize(new AutoMoqCustomization());
var assertion = new GuardClauseAssertion(fixture);
assertion.Verify(typeof(Address).GetConstructors());
Dies könnte noch weiter komprimiert werden:
typeof(Address).ShouldNotAcceptNullConstructorArguments();
Mit dieser Erweiterungsmethode:
public static void ShouldNotAcceptNullConstructorArguments(this Type type)
{
var fixture = new Fixture().Customize(new AutoMoqCustomization());
var assertion = new GuardClauseAssertion(fixture);
assertion.Verify(type.GetConstructors());
}
quelle
Antworten:
Genau für diese Art von Fällen habe ich eine t4-Vorlage erstellt. Um zu vermeiden, dass für unveränderliche Klassen viel Boilerplate geschrieben wird.
https://github.com/xaviergonz/T4Immutable T4Immutable ist eine T4-Vorlage für C # .NET-Apps, die Code für unveränderliche Klassen generiert.
Wenn Sie dies verwenden, sprechen Sie insbesondere von Nicht-Null-Tests:
Der Konstruktor wird dies sein:
Wenn Sie jedoch JetBrains Annotations für die Nullprüfung verwenden, können Sie auch Folgendes tun:
Und der Konstruktor wird dies sein:
Es gibt auch ein paar mehr Funktionen als diese.
quelle
Mit einem einfachen Refactoring können Sie einige Verbesserungen erzielen, die das Problem des Schreibens all dieser Zäune erleichtern. Zunächst benötigen Sie diese Erweiterungsmethode:
Sie können dann schreiben:
Wenn Sie den ursprünglichen Parameter in der Erweiterungsmethode zurückgeben, wird eine flüssige Schnittstelle erstellt, sodass Sie dieses Konzept bei Bedarf mit anderen Erweiterungsmethoden erweitern und alle in Ihrer Zuweisung verketten können.
Andere Techniken haben ein eleganteres Konzept, sind jedoch zunehmend komplexer in der Ausführung, z. B. das Dekorieren des Parameters mit einem
[NotNull]
Attribut und die Verwendung von Reflection wie folgt .Abgesehen davon benötigen Sie möglicherweise nicht alle diese Tests, es sei denn, Ihre Klasse ist Teil einer öffentlich zugänglichen API.
quelle
null
das Argument für den Konstruktor zu testen , wird kein fehlgeschlagener Test ausgegeben.throw
Äußere des Konstruktors zu verschieben. Sie übertragen nicht wirklich die Kontrolle innerhalb des Konstruktors an einen anderen Ort. Es ist nur eine nützliche Methode und eine Möglichkeit, Dinge flüssig zu machen , wenn Sie dies wünschen.Kurzfristig kann man nicht viel gegen die Mühsamkeit tun, solche Tests zu schreiben. Es gibt jedoch einige Hilfestellungen für Throw-Ausdrücke, die im Rahmen der nächsten Version von C # (v7) implementiert werden sollen, wahrscheinlich in den nächsten Monaten:
Sie können mit Throw-Ausdrücken über die Try Roslyn-Webapp experimentieren .
quelle
Ich bin überrascht, dass noch niemand NullGuard erwähnt hat . Es ist über NuGet verfügbar und speichert diese Null-Checks während der Kompilierungszeit automatisch in der IL.
Also wäre Ihr Konstruktorcode einfach
und NullGuard fügt diese Nullprüfungen hinzu, damit Sie sie in genau das umwandeln, was Sie geschrieben haben.
Beachten Sie aber, dass NullGuard ist Opt-out , das heißt, es wird diese Null - Kontrollen hinzuzufügen jede Methode und Konstruktorargument, Eigenschaft Getter und Setter und sogar überprüfen Methode Rückgabewerte , es sei denn Sie explizit erlauben einen Nullwert mit dem
[AllowNull]
Attribut.quelle
[NotNull]
Attributs funktioniert , anstatt zuzulassen, dass Null der Standardwert ist.Nein wahrscheinlich nicht. Was ist die Wahrscheinlichkeit, dass Sie das vermasseln werden? Wie groß ist die Wahrscheinlichkeit, dass sich eine bestimmte Semantik unter Ihnen ändert? Was ist die Auswirkung, wenn jemand es vermasselt?
Wenn Sie eine Menge Zeit damit verbringen, Tests für etwas durchzuführen, das selten kaputt geht, und wenn dies der Fall ist, ist es eine triviale Lösung ... vielleicht nicht wert.
Vielleicht? So etwas könnte man leicht mit Nachdenken machen. Es ist zu beachten, dass die Codegenerierung für den realen Code durchgeführt wird, sodass es keine N Klassen gibt, bei denen möglicherweise ein menschlicher Fehler vorliegt. Bug Prevention> Fehlererkennung.
quelle
if...throw
sie habenThrowHelper.ArgumentNull(myArg, "myArg")
, auf diese Weise die Logik immer noch da ist und Sie nicht auf Code - Coverage - dinged erhalten. Wo dieArgumentNull
Methode das machtif...throw
.Lohnt es sich, all diese Tests zu schreiben?
Nein,
weil ich ziemlich sicher bin, dass Sie diese Eigenschaften durch einige andere Logiktests getestet haben, bei denen diese Klassen verwendet werden.
Sie können beispielsweise Tests für die Factory-Klasse durchführen, deren Zusicherung auf diesen Eigenschaften basiert (
Name
z. B. Instanz, die mit einer ordnungsgemäß zugewiesenen Eigenschaft erstellt wurde).Wenn diese Klassen der öffentlichen API ausgesetzt sind, die von einem Drittanbieter / Endbenutzer verwendet wird (@EJoshua, danke für die Kenntnisnahme), können Tests für "erwartet
ArgumentNullException
" nützlich sein.Während Sie auf C # 7 warten, können Sie die Erweiterungsmethode verwenden
Zum Testen können Sie eine parametrisierte Testmethode verwenden, die mithilfe der Reflektion eine
null
Referenz für jeden Parameter erstellt und die erwartete Ausnahme bestätigt.quelle
Sie könnten immer eine Methode wie die folgende schreiben:
Dies erspart Ihnen zumindest Tipparbeit, wenn Sie über mehrere Methoden verfügen, für die diese Art der Validierung erforderlich ist. Bei dieser Lösung wird natürlich davon ausgegangen, dass keiner der Parameter Ihrer Methode zulässig ist.
null
Sie können dies jedoch ändern, um dies zu ändern, wenn Sie dies wünschen.Sie können dies auch erweitern, um eine andere typspezifische Validierung durchzuführen. Wenn Sie beispielsweise die Regel haben, dass Zeichenfolgen keine Leerzeichen oder Leerzeichen sein dürfen, können Sie die folgende Bedingung hinzufügen:
Die GetParameterInfo-Methode, auf die ich mich beziehe, übernimmt im Grunde genommen die Reflektion (damit ich nicht immer wieder dasselbe eingeben muss, was eine Verletzung des DRY-Prinzips darstellen würde ):
quelle
Ich schlage nicht vor, Ausnahmen vom Konstruktor auszulösen. Das Problem ist, dass Sie Schwierigkeiten haben werden, dies zu testen, da Sie gültige Parameter übergeben müssen, auch wenn diese für Ihren Test irrelevant sind.
Beispiel: Wenn Sie testen möchten, ob der dritte Parameter eine Ausnahme auslöst, müssen Sie den ersten und den zweiten Parameter korrekt übergeben. Ihr Test ist also nicht mehr isoliert. Wenn sich die Bedingungen des ersten und des zweiten Parameters ändern, schlägt dieser Testfall fehl, auch wenn der dritte Parameter getestet wird.
Ich schlage vor, die Java-Validierungs-API zu verwenden und den Validierungsprozess zu externalisieren. Ich schlage vor, vier Aufgaben (Klassen) zu haben:
Ein Vorschlagsobjekt mit mit Java-Validierung versehenen Parametern mit einer Validierungsmethode, die einen Satz von ConstraintViolations zurückgibt. Ein Vorteil ist, dass Sie diese Objekte umgehen können, ohne dass die Aussage gültig ist. Sie können die Überprüfung verzögern, bis sie erforderlich ist, ohne zu versuchen, das Domänenobjekt zu instanziieren. Das Validierungsobjekt kann in verschiedenen Ebenen verwendet werden, da es sich um ein POJO ohne schichtspezifische Kenntnisse handelt. Es kann Teil Ihrer öffentlichen API sein.
Eine Factory für das Domänenobjekt, die für die Erstellung gültiger Objekte verantwortlich ist. Sie übergeben das Vorschlagsobjekt und die Factory ruft die Validierung auf und erstellt das Domänenobjekt, wenn alles in Ordnung ist.
Das Domain-Objekt selbst, auf das für andere Entwickler zumeist nicht zugegriffen werden kann. Zu Testzwecken empfehle ich, diese Klasse im Paketumfang zu haben.
Eine Public Domain-Schnittstelle, um das konkrete Domain-Objekt zu verbergen und Missbrauch zu erschweren.
Ich schlage nicht vor, auf Nullwerte zu prüfen. Sobald Sie in die Domänenebene gelangen, müssen Sie nicht mehr null übergeben oder null zurückgeben, solange Sie keine Objektketten haben, die über End- oder Suchfunktionen für einzelne Objekte verfügen.
Ein weiterer Punkt: Das Schreiben von möglichst wenig Code ist kein Maßstab für die Codequalität. Denken Sie an 32.000 Spiele-Wettbewerbe. Dieser Code ist der kompakteste, aber auch der chaotischste Code, den Sie haben können, da er sich nur um technische Probleme und nicht um Semantik kümmert. Aber Semantik sind die Punkte, die die Dinge umfassend machen.
quelle