it-swarm.com.de

Gibt es eine einfachere Möglichkeit, die Argumentvalidierung und Feldinitialisierung in einem unveränderlichen Objekt zu testen?

Meine Domain besteht aus vielen einfachen unveränderlichen Klassen wie diesen:

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, aber derzeit schreibe ich Komponententests für jede dieser Klassen, um zu überprüfen, ob die Argumentvalidierung korrekt funktioniert und dass Alle Eigenschaften werden initialisiert. Dies fühlt sich nach extrem langweiliger Arbeit mit unangemessenem Nutzen an.

Die wirkliche Lösung wäre, dass C # die Unveränderlichkeit und nicht nullfähige Referenztypen nativ unterstützt. Aber was kann ich tun, um die Situation in der Zwischenzeit 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 das Schreiben von Tests für jede dieser Klassen zu vermeiden?


Hier ist, was ich jetzt basierend auf den Antworten habe.

Ich könnte die Nullprüfungen und die Eigenschaftsinitialisierung so vereinfachen, dass sie so aussehen:

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));

Verwendung 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 einfach mit dem GuardClauseAssertion von 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();

Verwenden 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());
}
21
Botond Balázs

Ich habe genau für diese Art von Fällen 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 Folgendes verwenden, sprechen Sie speziell von Nicht-Null-Tests:

[PreNotNullCheck, PostNotNullCheck]
public string FirstName { get; }

Der Konstruktor wird folgender sein:

public Person(string firstName) {
  // pre not null check
  if (firstName == null) throw new ArgumentNullException(nameof(firstName));

  // assignations + PostConstructor() if needed

  // post not null check
  if (this.FirstName == null) throw new NullReferenceException(nameof(this.FirstName));
}

Wenn Sie jedoch JetBrains Annotations für die Nullprüfung verwenden, können Sie Folgendes tun:

[JetBrains.Annotations.NotNull, ConstructorParamNotNull]
public string FirstName { get; }

Und der Konstruktor wird dies sein:

public Person([JetBrains.Annotations.NotNull] string firstName) {
  // pre not null check is implied by ConstructorParamNotNull
  if (firstName == null) throw new ArgumentNullException(nameof(firstName));

  FirstName = firstName;
  // + PostConstructor() if needed

  // post not null check implied by JetBrains.Annotations.NotNull on the property
  if (this.FirstName == null) throw new NullReferenceException(nameof(this.FirstName));
}

Es gibt auch ein paar mehr Funktionen als diese.

5
Javier G.

Mit einem einfachen Refactoring können Sie ein wenig Verbesserungen erzielen, um das Problem des Schreibens all dieser Zäune zu lösen. Zunächst benötigen Sie diese Erweiterungsmethode:

internal static T ThrowIfNull<T>(this T o, string paramName) where T : class
{
    if (o == null)
        throw new ArgumentNullException(paramName);

    return o;
}

Sie können dann schreiben:

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)
    {
        FullName = fullName.ThrowIfNull(nameof(fullName));
        NameAtBirth = nameAtBirth.ThrowIfNull(nameof(nameAtBirth));
        TaxId = taxId.ThrowIfNull(nameof(taxId));
        PhoneNumber = phoneNumber.ThrowIfNull(nameof(fullName));
        Address = address.ThrowIfNull(nameof(address));
    }
}

Wenn Sie den ursprünglichen Parameter in der Erweiterungsmethode zurückgeben, wird eine fließende Schnittstelle erstellt, sodass Sie dieses Konzept auf Wunsch mit anderen Erweiterungsmethoden erweitern und alle in Ihrer Zuweisung miteinander verketten können.

Andere Techniken sind im Konzept eleganter, aber in der Ausführung zunehmend komplexer, z. B. das Dekorieren des Parameters mit einem [NotNull] Attribut und mit Reflection wie this .

Möglicherweise benötigen Sie jedoch nicht alle diese Tests, es sei denn, Ihre Klasse ist Teil einer öffentlich zugänglichen API.

16
Robert Harvey

Kurzfristig können Sie nicht viel gegen die Langeweile 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 und voraussichtlich in den nächsten Monaten verfügbar sein werden:

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)
    {
        FullName = fullName ?? throw new ArgumentNullException(nameof(fullName));
        NameAtBirth = nameAtBirth ?? throw new ArgumentNullException(nameof(nameAtBirth));
        TaxId = taxId ?? throw new ArgumentNullException(nameof(taxId)); ;
        PhoneNumber = phoneNumber ?? throw new ArgumentNullException(nameof(phoneNumber)); ;
        Address = address ?? throw new ArgumentNullException(nameof(address)); ;
    }
}

Sie können mit Wurfausdrücken über die Try Roslyn-Webanwendung experimentieren.

7
David Arno

Ich bin überrascht, dass noch niemand NullGuard.Fody erwähnt hat. Es ist über NuGet verfügbar und verwebt diese Nullprüfungen während der Kompilierungszeit automatisch in die IL.

Ihr Konstruktorcode wäre also einfach

public Person(
    string fullName,
    string nameAtBirth,
    string taxId,
    PhoneNumber phoneNumber,
    Address address)
{
    FullName = fullName;
    NameAtBirth = nameAtBirth;
    TaxId = taxId;
    PhoneNumber = phoneNumber;
    Address = address;
}

und NullGuard fügt diese Nullprüfungen für Sie hinzu und wandelt sie in genau das um, was Sie geschrieben haben.

Beachten Sie jedoch, dass NullGuard opt-out ist, dh diese Nullprüfungen werden zu every Methoden- und Konstruktorargument, Eigenschafts-Getter und Setter und sogar hinzugefügt Überprüfen Sie die Rückgabewerte der Methode, es sei denn, Sie haben explizit allow einen Nullwert mit dem [AllowNull] Attribut.

7
Roman Reiner

Lohnt es sich, all diese Tests zu schreiben?

Nein wahrscheinlich nicht. Wie hoch ist die Wahrscheinlichkeit, dass Sie das vermasseln? Wie hoch ist die Wahrscheinlichkeit, dass sich einige Semantiken unter Ihnen ändern? 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 dies eine triviale Lösung ... vielleicht nicht wert.

Wäre es eine gute Idee, einen Codegenerator für solche Klassen zu schreiben, um das Schreiben von Tests für jede dieser Klassen zu vermeiden?

Könnte sein? So etwas könnte man leicht mit Reflexion machen. Zu berücksichtigen ist die Codegenerierung für den realen Code, sodass Sie keine N Klassen haben, die möglicherweise menschliches Versagen aufweisen. Fehlervermeidung> Fehlererkennung.

5
Telastyn

Sie können jederzeit eine Methode wie die folgende schreiben:

// Just to illustrate how to call this
private static void SomeMethod(string a, string b, string c, string d)
    {
        ValidateArguments(a, b, c, d);
        // ...
    }

    // This is the one to use as a utility function
    private static void ValidateArguments(params object[] args)
    {
        for (int i = 0; i < args.Length; i++)
        {
            if (args[i] == null)
            {
                StackTrace trace = new StackTrace();
                // Get the method that called us
                MethodBase info = trace.GetFrame(1).GetMethod();

                // Get information on the parameter that is null so we can add its name to the exception
                ParameterInfo param = info.GetParameters()[i];

                // Raise the exception on behalf of the caller
                throw new ArgumentNullException(param.Name);
            }
        }
    }

Dies erspart Ihnen zumindest einige Eingaben, wenn Sie über mehrere Methoden verfügen, die diese Art der Validierung erfordern. Bei dieser Lösung wird natürlich davon ausgegangen, dass keiner der Parameter Ihrer Methode null sein kann, aber Sie können dies ändern, um dies zu ändern, wenn Sie dies möchten Verlangen.

Sie können dies auch erweitern, um eine andere typspezifische Validierung durchzuführen. Wenn Sie beispielsweise die Regel haben, dass Zeichenfolgen nicht nur Leerzeichen oder Leerzeichen sein dürfen, können Sie die folgende Bedingung hinzufügen:

// Note that we already know based on the previous condition that args[i] is not null
else if (args[i].GetType() == typeof(string))
            {
                string argCast = arg as string;

                if (!argCast.Trim().Any())
                {
                    ParameterInfo param = GetParameterInfo(i);

                    throw new ArgumentException(param.Name + " is empty or consists only of whitespace");
                }
            }

Die GetParameterInfo-Methode, auf die ich mich beziehe, führt im Grunde genommen die Reflexion durch (damit ich nicht immer wieder dasselbe tippen muss, was eine Verletzung des DRY-Prinzips wäre):

private static ParameterInfo GetParameterInfo(int index)
    {
        StackTrace trace = new StackTrace();

        // Note that we have to go 2 methods back to get the ValidateArguments method's caller
        MethodBase info = trace.GetFrame(2).GetMethod();

        // Get information on the parameter that is null so we can add its name to the exception
        ParameterInfo param = info.GetParameters()[index];

        return param;
    }

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.

Beispielsweise können Sie Tests für die Factory-Klasse durchführen lassen, deren Bestätigung auf diesen Eigenschaften basiert (z. B. Instanz, die mit der ordnungsgemäß zugewiesenen Eigenschaft Name erstellt wurde).

Wenn diese Klassen der öffentlichen API ausgesetzt sind, die von einem dritten Teil/Endbenutzer verwendet wird (@EJoshua, danke für die Benachrichtigung), können Tests für das erwartete ArgumentNullException hilfreich sein.

Während Sie auf C # 7 warten, können Sie die Erweiterungsmethode verwenden

public MyClass(string name)
{
    name.ThrowArgumentNullExceptionIfNull(nameof(name));
}

public static void ThrowArgumentNullExceptionIfNull(this object value, string paramName)
{
    if(value == null)
        throw new ArgumentNullException(paramName);
}

Zum Testen können Sie eine parametrisierte Testmethode verwenden, die mithilfe der Reflexion für jeden Parameter eine null Referenz erstellt und für die erwartete Ausnahme bestätigt.

2
Fabio

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 korrekt übergeben. Ihr Test ist also nicht mehr isoliert. Wenn sich die Einschränkungen des ersten und zweiten Parameters ändern, schlägt dieser Testfall fehl, selbst wenn der dritte Parameter getestet wird.

Ich schlage vor, die Validierungs-API Java) zu verwenden und den Validierungsprozess zu externalisieren. Ich schlage vor, vier Verantwortlichkeiten (Klassen) zu haben:

  1. Ein Vorschlagsobjekt mit Java Validierung annotierte Parameter mit einer Validierungsmethode, die einen Satz von ConstraintViolations zurückgibt. Ein Vorteil besteht darin, dass Sie diese Objekte ohne die Zusicherung übergeben können, dass sie gültig sind. Sie können die Validierung bis dahin verzögern Dies ist erforderlich, ohne dass versucht werden muss, das Domänenobjekt zu instanziieren. Das Validierungsobjekt kann in verschiedenen Ebenen verwendet werden, da es sich um ein POJO ohne schichtenspezifisches Wissen handelt. Es kann Teil Ihrer öffentlichen API sein.

  2. Eine Factory für das Domänenobjekt, das 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.

  3. Das Domänenobjekt selbst, auf das andere Entwickler für die Instanziierung meistens nicht zugreifen können sollten. Zu Testzwecken empfehle ich, diese Klasse im Paketumfang zu haben.

  4. Eine gemeinfreie Schnittstelle, um das konkrete Domänenobjekt auszublenden und Missbrauch zu erschweren.

Ich schlage nicht vor, nach Nullwerten zu suchen. Sobald Sie in die Domänenebene gelangen, müssen Sie nicht mehr null übergeben oder null zurückgeben, solange Sie keine Objektketten mit Enden oder Suchfunktionen für einzelne Objekte haben.

Ein weiterer Punkt: Das Schreiben von so wenig Code wie möglich ist keine Metrik 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 die Semantik kümmert. Aber Semantik sind die Punkte, die die Dinge umfassend machen.

0
oopexpert