Wie soll ich einen Test für eine reine Methode schreiben, die nichts zurückgibt?

13

Ich habe eine Reihe von Klassen, die sich mit der Validierung von Werten befassen. Beispielsweise RangeValidatorprüft eine Klasse, ob ein Wert innerhalb des angegebenen Bereichs liegt.

Jede Validator-Klasse enthält zwei Methoden: eine is_valid(value), die einen bestimmten Wert zurückgibt Trueoder von diesem Falseabhängig ist und ensure_valid(value)die nach einem bestimmten Wert sucht und entweder nichts unternimmt, wenn der Wert gültig ist, oder eine bestimmte Ausnahme auslöst, wenn der Wert nicht den vordefinierten Regeln entspricht.

Mit dieser Methode sind derzeit zwei Komponententests verbunden:

  • Derjenige, der einen ungültigen Wert übergibt und sicherstellt, dass die Ausnahme ausgelöst wurde.

    def test_outside_range(self):
        with self.assertRaises(demo.ValidationException):
            demo.RangeValidator(0, 100).ensure_valid(-5)
    
  • Derjenige, der einen gültigen Wert übergibt.

    def test_in_range(self):
        demo.RangeValidator(0, 100).ensure_valid(25)
    

Obwohl der zweite Test seine Aufgabe erfüllt - schlägt fehl, wenn die Ausnahme ausgelöst wird, und ist erfolgreich, wenn ensure_validnichts ausgelöst wird -, sieht die Tatsache, dass kein asserts enthalten ist, seltsam aus. Jemand, der solchen Code liest, würde sich sofort fragen, warum es einen Test gibt, der scheinbar nichts tut.

Ist dies eine aktuelle Praxis beim Testen von Methoden, die keinen Wert zurückgeben und keine Nebenwirkungen haben? Oder sollte ich den Test anders schreiben? Oder einfach einen Kommentar hinterlassen, der erklärt, was ich tue?

Arseni Mourzenko
quelle
11
Pedantischer Punkt, aber wenn Sie eine Funktion haben, die keine Argumente akzeptiert (außer als selfReferenz) und kein Ergebnis zurückgibt, ist dies keine reine Funktion.
David Arno
10
@DavidArno: Dies ist kein umständlicher Punkt, es geht direkt zum Kern der Frage: Die Methode ist schwer zu testen, genau weil sie unrein ist.
Jörg W Mittag
@DavidArno Pedantischer ist, dass Sie die Methode "do nothing" verwenden können (vorausgesetzt, "return no results" wird als "return a unit type" interpretiert, der voidin vielen Sprachen aufgerufen wird und alberne Regeln enthält.). Alternativ können Sie eine unendliche Zahl angeben Schleife (die auch als „Rückkehr kein Ergebnis“ funktioniert wirklich bedeutet , lieferte keine Ergebnisse.
Derek Elkins links SE
In jeder Sprache gibt es eine reine Funktion vom Typ, unit -> unitdie Einheit als Standarddatentyp betrachtet, anstatt etwas magisch Besonderes. Leider gibt es nur Einheit zurück und tut nichts anderes.
Phoshi

Antworten:

21

Die meisten Test-Frameworks haben eine explizite Aussage für " expect(() => {}).not.toThrow();Wirft nicht", z. B. hat Jasmine und nUnit und Freunde haben auch eine.

DeadMG
quelle
1
Und wenn das Test-Framework keine solche Aussage hat, ist es immer möglich, eine lokale Methode zu erstellen, die genau dasselbe tut und den Code selbsterklärend macht. Gute Idee.
Arseni Mourzenko
7

Dies hängt stark von der verwendeten Sprache und dem verwendeten Framework ab. Apropos NUnit, es gibt Assert.Throws(...)Methoden. Sie können ihnen eine Lambda-Methode übergeben:

Assert.Throws(() => rangeValidator.EnsureValid(-5))

die innerhalb der ausgeführt wird Assert.Throws. Der Aufruf des Lambda wird höchstwahrscheinlich von einem try { } catch { }Block eingeschlossen, und die Assertion schlägt fehl, wenn eine Ausnahme abgefangen wird.

Wenn Ihr Framework diese Mittel nicht bietet, können Sie es umgehen, indem Sie den Aufruf selbst abschließen (ich schreibe in C #):

// assert that the method does not fail
try
{
    rangeValidator.EnsureValid(50);
}
catch(Exception e)
{
    Assert.IsTrue(false, $"Exception: {e.Message}");
}

// assert that the method does fail
try
{
    rangeValidator.EnsureValid(50);
    Assert.IsTrue(false, $"Method is expected to throw an exception");
}
catch(Exception e)
{
}

Dadurch wird die Absicht klarer, aber der Code wird bis zu einem gewissen Grad unübersichtlich. (Natürlich können Sie all diese Dinge in eine Methode einwickeln.) Am Ende liegt es an Ihnen.

Bearbeiten

Wie Doc Brown in den Kommentaren hervorhob, bestand das Problem nicht darin, anzuzeigen, dass die Methode ausgelöst wird, sondern, dass sie nicht ausgelöst wird. In NUnit gibt es auch eine Behauptung dafür

Assert.DoesNotThrow(() => rangeValidator.EnsureValid(-5))
Paul Kertscher
quelle
1

Fügen Sie einfach einen Kommentar hinzu, um zu verdeutlichen, warum keine Zusicherung erforderlich ist und warum Sie sie nicht vergessen haben.

Wie Sie in den anderen Antworten sehen können, macht alles andere den Code komplizierter und übersichtlicher. Mit dem Kommentar dort werden andere Programmierer die Absicht des Tests kennen.

Davon abgesehen sollte diese Art von Test eine Ausnahme sein (kein Wortspiel beabsichtigt). Wenn Sie feststellen, dass Sie so etwas regelmäßig schreiben, werden Sie wahrscheinlich anhand der Tests feststellen, dass das Design nicht optimal ist.

jhyot
quelle
1

Sie können auch behaupten, dass einige Methoden ordnungsgemäß aufgerufen (oder nicht aufgerufen) werden.

Zum Beispiel:

public void SendEmail(User user)
     ... construct email ...
     _emailSender.Send(email);

In Ihrem Test:

emailSenderMock.VerifyIgnoreArgs(service =>
    service.Send(It.IsAny<Email>()),
    Times.Once
);
Gabriel Robert
quelle