it-swarm-eu.dev

Ist es in Ordnung, mehrere Asserts in einem einzigen Unit-Test zu haben?

In dem Kommentar zu diesem großartigen Beitrag erwähnte Roy Osherove das OAPT Projekt, mit dem jede Behauptung in einem einzigen Test ausgeführt werden soll.

Folgendes ist auf der Homepage des Projekts geschrieben:

Richtige Komponententests sollten aus genau einem Grund fehlschlagen. Deshalb sollten Sie eine Bestätigung pro Komponententest verwenden.

Und Roy schrieb auch in Kommentaren:

Meine Richtlinie ist normalerweise, dass Sie ein logisches KONZEPT pro Test testen. Sie können mehrere Asserts für dasselbe Objekt haben. Sie werden normalerweise das gleiche Konzept sein, das getestet wird.

Ich denke, dass es einige Fälle gibt, in denen mehrere Behauptungen erforderlich sind (z. B. Guard Assertion ), aber im Allgemeinen versuche ich dies zu vermeiden. Was ist deine Meinung? Bitte geben Sie ein Beispiel aus der Praxis an, in dem mehrere Asserts wirklich benötigt sind.

430
Restuta

Ich denke nicht, dass es notwendigerweise eine schlechte Sache ist , aber ich denke, wir sollten danach streben, nur einzelne Behauptungen zu haben in unseren Tests. Dies bedeutet, dass Sie viel mehr Tests schreiben und unsere Tests immer nur eine Sache gleichzeitig testen würden.

Trotzdem würde ich sagen, dass vielleicht die Hälfte meiner Tests nur eine Behauptung hat. Ich denke, es wird nur dann ein Code (Test?) Geruch , wenn Sie ungefähr fünf oder mehr Asserts in Ihrem Test haben.

Wie lösen Sie mehrere Behauptungen?

246
Jaco Pretorius

Tests sollten nur aus einem Grund fehlschlagen, aber das bedeutet nicht immer, dass es nur eine Assert -Anweisung geben sollte. IMHO ist es wichtiger, an dem Muster " Arrange, Act, Assert " festzuhalten.

Der Schlüssel ist, dass Sie nur eine Aktion haben und dann die Ergebnisse dieser Aktion mithilfe von Asserts überprüfen. Aber es ist "Arrangieren, handeln, behaupten, Ende des Tests". Wenn Sie versucht sind, den Test fortzusetzen, indem Sie eine andere Aktion ausführen und anschließend weitere Asserts ausführen, machen Sie diesen Test stattdessen separat.

Ich freue mich über mehrere Assert-Anweisungen, die Teil des Testens derselben Aktion sind. z.B.

[Test]
public void ValueIsInRange()
{
  int value = GetValueToTest();

  Assert.That(value, Is.GreaterThan(10), "value is too small");
  Assert.That(value, Is.LessThan(100), "value is too large");
} 

oder

[Test]
public void ListContainsOneValue()
{
  var list = GetListOf(1);

  Assert.That(list, Is.Not.Null, "List is null");
  Assert.That(list.Count, Is.EqualTo(1), "Should have one item in list");
  Assert.That(list[0], Is.Not.Null, "Item is null");
} 

Sie könnten kombinieren diese zu einer Behauptung, aber das ist etwas anderes als darauf zu bestehen, dass Sie sollten oder müssen. Es gibt keine Verbesserung, wenn man sie kombiniert.

z.B. Der erste könnte sein

Assert.IsTrue((10 < value) && (value < 100), "Value out of range"); 

Dies ist jedoch nicht besser - die Fehlermeldung ist weniger spezifisch und hat keine weiteren Vorteile. Ich bin sicher, Sie können sich andere Beispiele vorstellen, bei denen das Kombinieren von zwei oder drei (oder mehr) Behauptungen zu einer großen booleschen Bedingung das Lesen, das Ändern und das Herausfinden von Fehlern erschwert. Warum nur aus Gründen der Regel?

[~ # ~] nb [~ # ~] : Der Code, den ich hier schreibe, ist C # mit NUnit, aber die Prinzipien gelten auch für andere Sprachen und Frameworks. Die Syntax kann auch sehr ähnlich sein.

314
Anthony

Ich hätte nie gedacht, dass mehr als eine Behauptung eine schlechte Sache ist.

Das mache ich die ganze Zeit:

public void ToPredicateTest()
{
    ResultField rf = new ResultField(ResultFieldType.Measurement, "name", 100);
    Predicate<ResultField> p = (new ConditionBuilder()).LessThanConst(400)
                                                       .Or()
                                                       .OpenParenthesis()
                                                       .GreaterThanConst(500)
                                                       .And()
                                                       .LessThanConst(1000)
                                                       .And().Not()
                                                       .EqualsConst(666)
                                                       .CloseParenthesis()
                                                       .ToPredicate();
    Assert.IsTrue(p(ResultField.FillResult(rf, 399)));
    Assert.IsTrue(p(ResultField.FillResult(rf, 567)));
    Assert.IsFalse(p(ResultField.FillResult(rf, 400)));
    Assert.IsFalse(p(ResultField.FillResult(rf, 666)));
    Assert.IsFalse(p(ResultField.FillResult(rf, 1001)));

    Predicate<ResultField> p2 = (new ConditionBuilder()).EqualsConst(true).ToPredicate();

    Assert.IsTrue(p2(new ResultField(ResultFieldType.Confirmation, "Is True", true)));
    Assert.IsFalse(p2(new ResultField(ResultFieldType.Confirmation, "Is False", false)));
}

Hier verwende ich mehrere Asserts, um sicherzustellen, dass komplexe Bedingungen in das erwartete Prädikat umgewandelt werden können.

Ich teste nur eine Einheit (die ToPredicate -Methode), aber ich decke alles ab, was mir im Test einfällt.

85
Matt Ellen

Wenn ich Unit-Tests verwende, um das Verhalten auf hoher Ebene zu validieren, habe ich absolut mehrere Aussagen in einem einzigen Test zusammengefasst. Hier ist ein Test, den ich tatsächlich für einen Notfallbenachrichtigungscode verwende. Der Code, der vor dem Test ausgeführt wird, versetzt das System in einen Zustand, in dem ein Alarm gesendet wird, wenn der Hauptprozessor ausgeführt wird.

@Test
public void testAlarmSent() {
    assertAllUnitsAvailable();
    assertNewAlarmMessages(0);

    pulseMainProcessor();

    assertAllUnitsAlerting();
    assertAllNotificationsSent();
    assertAllNotificationsUnclosed();
    assertNewAlarmMessages(1);
}

Es stellt die Bedingungen dar, die bei jedem Schritt des Prozesses vorhanden sein müssen, damit ich sicher sein kann, dass sich der Code so verhält, wie ich es erwartet habe. Wenn eine einzelne Behauptung fehlschlägt, ist es mir egal, dass die verbleibenden nicht einmal ausgeführt werden. Da der Status des Systems nicht mehr gültig ist, würden mir diese nachfolgenden Behauptungen nichts Wertvolles sagen. * Wenn assertAllUnitsAlerting() fehlgeschlagen wäre, würde ich nicht wissen, was ich von assertAllNotificationSent() 's halten soll Erfolg OR Fehler, bis ich festgestellt habe, was den vorherigen Fehler verursacht hat, und ihn korrigiert habe.

(* - Okay, sie könnten möglicherweise beim Debuggen des Problems hilfreich sein. Die wichtigsten Informationen, dass der Test fehlgeschlagen ist, wurden jedoch bereits empfangen.)

21
BlairHippo

Ein weiterer Grund, warum ich denke, dass mehrere Asserts in einer Methode keine schlechte Sache sind, wird im folgenden Code beschrieben:

class Service {
    Result process();
}

class Result {
    Inner inner;
}

class Inner {
    int number;
}

In meinem Test möchte ich einfach testen, ob service.process() in Instanzen der Klasse Inner die richtige Nummer zurückgibt.

Anstatt zu testen ...

@Test
public void test() {
    Result res = service.process();
    if ( res != null && res.getInner() != null ) Assert.assertEquals( ..., res.getInner() );
}

Ich mache

@Test
public void test() {
    Result res = service.process();
    Assert.notNull(res);
    Assert.notNull(res.getInner());
    Assert.assertEquals( ..., res.getInner() );
}
8
Betlista

Ich denke, es gibt viele Fälle, in denen das Schreiben mehrerer Asserts innerhalb der Regel gültig ist, dass ein Test nur aus einem Grund fehlschlagen sollte.

Stellen Sie sich beispielsweise eine Funktion vor, die eine Datumszeichenfolge analysiert:

function testParseValidDateYMD() {
    var date = Date.parse("2016-01-02");

    Assert.That(date.Year).Equals(2016);
    Assert.That(date.Month).Equals(1);
    Assert.That(date.Day).Equals(0);
}

Wenn der Test aus einem Grund fehlschlägt, ist die Analyse falsch. Wenn Sie argumentieren würden, dass dieser Test aus drei verschiedenen Gründen fehlschlagen kann, wären Sie meiner Meinung nach in Ihrer Definition von "einem Grund" zu feinkörnig.

6
Pete

Ich kenne keine Situation, in der es eine gute Idee wäre, mehrere Behauptungen innerhalb der [Test] -Methode selbst zu haben. Der Hauptgrund, warum Leute gerne mehrere Assertions haben, ist, dass sie versuchen, eine [TestFixture] -Klasse für jede getestete Klasse zu haben. Stattdessen können Sie Ihre Tests in mehrere [TestFixture] -Klassen aufteilen. Auf diese Weise können Sie mehrere Arten sehen, in denen der Code möglicherweise nicht wie erwartet reagiert hat, anstatt nur die, bei der die erste Behauptung fehlgeschlagen ist. Auf diese Weise erreichen Sie mindestens ein Verzeichnis pro Klasse, das mit vielen [TestFixture] -Klassen getestet wird. Jede [TestFixture] -Klasse wird nach dem spezifischen Status eines Objekts benannt, das Sie testen möchten. Die [SetUp] -Methode versetzt das Objekt in den durch den Klassennamen beschriebenen Zustand. Dann haben Sie mehrere [Test] -Methoden, die jeweils unterschiedliche Aussagen machen, die Sie angesichts des aktuellen Status des Objekts als wahr erwarten würden. Jede [Test] -Methode ist nach der Sache benannt, die sie behauptet, außer vielleicht wird sie nach dem Konzept benannt, anstatt nur nach einer englischen Auslesung des Codes. Dann benötigt jede [Test] -Methodenimplementierung nur eine einzige Codezeile, in der etwas behauptet wird. Ein weiterer Vorteil dieses Ansatzes besteht darin, dass die Tests sehr gut lesbar sind, da anhand der Klassen- und Methodennamen deutlich wird, was Sie testen und was Sie erwarten. Dies lässt sich auch besser skalieren, wenn Sie beginnen, alle kleinen Edge-Fälle zu erkennen, die Sie testen möchten, und wenn Sie Fehler finden.

Normalerweise bedeutet dies, dass die letzte Codezeile in der [SetUp] -Methode einen Eigenschaftswert oder einen Rückgabewert in einer privaten Instanzvariablen des [TestFixture] speichern sollte. Dann können Sie mehrere verschiedene Dinge über diese Instanzvariable aus verschiedenen [Test] -Methoden behaupten. Sie können auch Aussagen darüber machen, welche unterschiedlichen Eigenschaften des zu testenden Objekts jetzt eingestellt sind, da es sich im gewünschten Zustand befindet.

Manchmal müssen Sie unterwegs Aussagen treffen, während Sie das zu testende Objekt in den gewünschten Zustand versetzen, um sicherzustellen, dass Sie nichts falsch gemacht haben, bevor Sie das Objekt in den gewünschten Zustand versetzen. In diesem Fall sollten diese zusätzlichen Zusicherungen in der [SetUp] -Methode angezeigt werden. Wenn innerhalb der [SetUp] -Methode etwas schief geht, ist klar, dass beim Test etwas nicht stimmte, bevor das Objekt jemals in den gewünschten Zustand überging, den Sie testen wollten.

Ein weiteres Problem, auf das Sie möglicherweise stoßen, besteht darin, dass Sie möglicherweise eine Ausnahme testen, von der Sie erwartet haben, dass sie ausgelöst wird. Dies kann Sie dazu verleiten, dem obigen Modell nicht zu folgen. Dies kann jedoch weiterhin erreicht werden, indem die Ausnahme innerhalb der [SetUp] -Methode abgefangen und in einer Instanzvariablen gespeichert wird. Auf diese Weise können Sie verschiedene Dinge über die Ausnahme behaupten, jede in ihrer eigenen [Test] -Methode. Sie können dann auch andere Dinge über das zu testende Objekt geltend machen, um sicherzustellen, dass die ausgelöste Ausnahme keine unbeabsichtigten Nebenwirkungen hat.

Beispiel (dies würde in mehrere Dateien aufgeteilt):

namespace Tests.AcctTests
{
    [TestFixture]
    public class no_events
    {
        private Acct _acct;

        [SetUp]
        public void SetUp() {
            _acct = new Acct();
        }

        [Test]
        public void balance_0() {
            Assert.That(_acct.Balance, Is.EqualTo(0m));
        }
    }

    [TestFixture]
    public class try_withdraw_0
    {
        private Acct _acct;
        private List<string> _problems;

        [SetUp]
        public void SetUp() {
            _acct = new Acct();
            Assert.That(_acct.Balance, Is.EqualTo(0));
            _problems = _acct.Withdraw(0m);
        }

        [Test]
        public void has_problem() {
            Assert.That(_problems, Is.EquivalentTo(new string[] { "Withdraw amount must be greater than zero." }));
        }

        [Test]
        public void balance_not_changed() {
            Assert.That(_acct.Balance, Is.EqualTo(0m));
        }
    }

    [TestFixture]
    public class try_withdraw_negative
    {
        private Acct _acct;
        private List<string> _problems;

        [SetUp]
        public void SetUp() {
            _acct = new Acct();
            Assert.That(_acct.Balance, Is.EqualTo(0));
            _problems = _acct.Withdraw(-0.01m);
        }

        [Test]
        public void has_problem() {
            Assert.That(_problems, Is.EquivalentTo(new string[] { "Withdraw amount must be greater than zero." }));
        }

        [Test]
        public void balance_not_changed() {
            Assert.That(_acct.Balance, Is.EqualTo(0m));
        }
    }
}
3

Wenn Sie mehrere Asserts in einer einzelnen Testfunktion haben, erwarte ich, dass diese für den von Ihnen durchgeführten Test direkt relevant sind. Zum Beispiel,

@Test
test_Is_Date_segments_correct {

   // It is okay if you have multiple asserts checking dd, mm, yyyy, hh, mm, ss, etc. 
   // But you would not have any assert statement checking if it is string or number,
   // that is a different test and may be with multiple or single assert statement.
}

Viele Tests zu haben (auch wenn Sie das Gefühl haben, dass es wahrscheinlich ein Overkill ist) ist keine schlechte Sache. Sie können argumentieren, dass es wichtiger ist, die wichtigsten und wichtigsten Tests zu haben. Wenn Sie also behaupten, stellen Sie sicher, dass Ihre Assert-Anweisungen korrekt platziert sind, anstatt sich über zu viele Asserts Gedanken zu machen. Wenn Sie mehr als eine benötigen, verwenden Sie mehr als eine.

2
hagubear

Mehrere Zusicherungen im selben Test sind nur dann ein Problem, wenn der Test fehlschlägt. Dann müssen Sie möglicherweise den Test debuggen oder die Ausnahme analysieren, um herauszufinden, welche Behauptung fehlschlägt. Mit einer Aussage in jedem Test ist es normalerweise einfacher festzustellen, was falsch ist.

Ich kann mir kein Szenario vorstellen, in dem mehrere Behauptungen wirklich erforderlich sind, da Sie sie immer als mehrere Bedingungen in derselben Behauptung umschreiben können. Es kann jedoch vorzuziehen sein, wenn Sie beispielsweise mehrere Schritte haben, um die Zwischendaten zwischen den Schritten zu überprüfen, anstatt zu riskieren, dass die späteren Schritte aufgrund einer schlechten Eingabe abstürzen.

2
Guffa

Wenn Ihr Test fehlschlägt, wissen Sie nicht, ob die folgenden Aussagen ebenfalls fehlerhaft sind. Oft bedeutet dies, dass Ihnen wertvolle Informationen fehlen, um die Ursache des Problems herauszufinden. Meine Lösung besteht darin, eine Behauptung mit mehreren Werten zu verwenden:

String actual = "val1="+val1+"\nval2="+val2;
assertEquals(
    "val1=5\n" +
    "val2=hello"
    , actual
);

Dadurch kann ich alle fehlgeschlagenen Behauptungen auf einmal sehen. Ich verwende mehrere Zeilen, da die meisten IDEs Zeichenfolgenunterschiede in einem Vergleichsdialog nebeneinander anzeigen.

2
Aaron Digulla

Ziel des Unit-Tests ist es, Ihnen so viele Informationen wie möglich über Fehler zu geben, aber auch dabei zu helfen, die grundlegendsten Probleme zuerst genau zu lokalisieren. Wenn Sie logischerweise wissen, dass eine Zusicherung fehlschlägt, wenn eine andere Zusicherung fehlschlägt, oder mit anderen Worten, es besteht eine Abhängigkeitsbeziehung zwischen dem Test, ist es sinnvoll, diese als mehrere Zusicherungen innerhalb eines einzelnen Tests zu rollen. Dies hat den Vorteil, dass die Testergebnisse nicht mit offensichtlichen Fehlern übersät sind, die hätten beseitigt werden können, wenn wir bei der ersten Behauptung innerhalb eines einzelnen Tests gerettet hätten. In dem Fall, in dem diese Beziehung nicht besteht, besteht die Präferenz natürlich darin, diese Behauptungen in einzelne Tests zu unterteilen, da das Auffinden dieser Fehler andernfalls mehrere Iterationen von Testläufen erfordern würde, um alle Probleme zu lösen.

Wenn Sie dann die Einheiten/Klassen auch so entwerfen, dass übermäßig komplexe Tests geschrieben werden müssten, wird dies beim Testen weniger belastet und fördert wahrscheinlich ein besseres Design.

1
jpierson

Ja, es ist in Ordnung, mehrere Behauptungen aufzustellen solange Ein fehlgeschlagener Test liefert Ihnen genügend Informationen, um den Fehler diagnostizieren zu können. Dies hängt davon ab, was Sie testen und welche Fehlermodi es gibt.

Richtige Komponententests sollten aus genau einem Grund fehlschlagen. Deshalb sollten Sie eine Bestätigung pro Komponententest verwenden.

Ich habe solche Formulierungen nie als hilfreich empfunden (dass eine Klasse einen Grund zur Änderung haben sollte, ist ein Beispiel für ein so wenig hilfreiches Sprichwort). Stellen Sie sich eine Behauptung vor, dass zwei Zeichenfolgen gleich sind. Dies entspricht semantisch der Behauptung, dass die Länge der beiden Zeichenfolgen gleich ist und jedes Zeichen am entsprechenden Index gleich ist.

Wir könnten verallgemeinern und sagen, dass jedes System mehrerer Behauptungen als eine einzige Behauptung umgeschrieben werden könnte und jede einzelne Behauptung in eine Reihe kleinerer Behauptungen zerlegt werden könnte.

Konzentrieren Sie sich also nur auf die Klarheit des Codes und die Klarheit der Testergebnisse und lassen Sie sich davon leiten, wie viele Aussagen Sie verwenden, und nicht umgekehrt.

1
CurtainDog

Diese Frage bezieht sich auf das klassische Problem des Ausgleichs zwischen Spaghetti und Lasagne-Code-Problemen.

Wenn Sie mehrere Asserts haben, kann dies leicht zu einem Spaghetti-Problem führen, bei dem Sie keine Ahnung haben, worum es bei dem Test geht. Wenn Sie jedoch nur einen Assert pro Test haben, kann dies dazu führen, dass Ihre Tests ebenso unlesbar sind, wenn Sie mehrere Tests in einer großen Lasagne durchführen, um herauszufinden, welcher Test was unmöglich macht .

Es gibt einige Ausnahmen, aber in diesem Fall ist es die Antwort, das Pendel in der Mitte zu halten.

0
gsf

Die Antwort ist sehr einfach: Wenn Sie eine Funktion testen, die mehr als ein Attribut desselben Objekts oder sogar zwei verschiedene Objekte ändert und die Richtigkeit der Funktion von den Ergebnissen all dieser Änderungen abhängt, möchten Sie dies bestätigen dass jede dieser Änderungen ordnungsgemäß durchgeführt wurde!

Ich habe die Idee eines logischen Konzepts, aber die umgekehrte Schlussfolgerung würde sagen, dass keine Funktion jemals mehr als ein Objekt ändern darf. Nach meiner Erfahrung ist dies jedoch nicht in allen Fällen umzusetzen.

Nehmen Sie das logische Konzept einer Banküberweisung - das Abheben eines Betrags von einem Bankkonto muss in den meisten Fällen das Hinzufügen dieses Betrags zu einem anderen Konto beinhalten. Sie wollen diese beiden Dinge NIEMALS trennen, sie bilden eine atomare Einheit. Möglicherweise möchten Sie zwei Funktionen ausführen (Zurückziehen/Hinzufügen von Geld) und somit zwei verschiedene Komponententests schreiben - zusätzlich. Diese beiden Aktionen müssen jedoch innerhalb einer Transaktion ausgeführt werden, und Sie möchten auch sicherstellen, dass die Transaktion funktioniert. In diesem Fall reicht es einfach nicht aus, um sicherzustellen, dass die einzelnen Schritte erfolgreich waren. Sie müssen in Ihrem Test beide Bankkonten überprüfen.

Möglicherweise gibt es komplexere Beispiele, die Sie zunächst nicht in einem Komponententest, sondern in einem Integrations- oder Abnahmetest testen würden. Aber diese Grenzen sind fließend, IMHO! Es ist nicht so einfach zu entscheiden, es ist eine Frage der Umstände und vielleicht der persönlichen Vorlieben. Das Abheben von Geld von einem Konto und das Hinzufügen zu einem anderen Konto ist immer noch eine sehr einfache Funktion und definitiv ein Kandidat für Unit-Tests.

0
cslotty