Gewusst wie: Unit-Test-Methode, die eine Auflistung zurückgibt, während Logik im Test vermieden wird

14

Ich teste eine Methode, die eine Sammlung von Datenobjekten erzeugen soll. Ich möchte überprüfen, ob die Eigenschaften der Objekte korrekt eingestellt sind. Einige der Eigenschaften werden auf dasselbe festgelegt. andere werden auf einen Wert gesetzt, der von ihrer Position in der Sammlung abhängt. Der natürliche Weg, dies zu tun, scheint eine Schleife zu sein. Roy Osherove rät jedoch nachdrücklich von der Verwendung von Logik in Komponententests ab ( Art of Unit Testing , 178). Er sagt:

Ein Test, der Logik enthält, testet normalerweise mehr als eine Sache gleichzeitig, was nicht empfohlen wird, da der Test weniger lesbar und anfälliger ist. Die Testlogik erhöht jedoch auch die Komplexität, die einen versteckten Fehler enthalten kann.

Tests sollten in der Regel eine Reihe von Methodenaufrufen ohne Kontrollflüsse (auch nicht) try-catchund mit Assert-Aufrufen sein.

An meinem Design kann ich jedoch nichts Falsches erkennen (wie sonst generieren Sie eine Liste von Datenobjekten, von deren Werten einige in der Reihenfolge abhängen, in der sie sich befinden? - Sie können sie nicht exakt separat generieren und testen). Gibt es etwas nicht testfreundliches an meinem Design? Oder widme ich mich zu sehr Osheroves Lehre? Oder gibt es eine geheime Unit-Test-Magie, die ich nicht kenne, um dieses Problem zu umgehen? (Ich schreibe in C # / VS2010 / NUnit, suche aber wenn möglich nach sprachunabhängigen Antworten.)

Kazark
quelle
4
Ich empfehle, nicht zu schleifen. Wenn Ihr Test lautet, dass der Balken des dritten Objekts auf Frob eingestellt ist, schreiben Sie einen Test, um speziell zu überprüfen, ob der Balken des dritten Objekts Frob ist. Das ist ein Test für sich, gehen Sie direkt zu ihm, keine Schleife. Wenn Ihr Test ist, dass Sie eine Sammlung von 5 Dingen erhalten, ist das auch ein Test. Das heißt nicht, dass Sie niemals eine Schleife haben (explizit oder anderweitig), sondern nur, dass Sie dies nicht oft brauchen. Behandeln Sie das Buch von Osherove auch als mehr Richtlinien als tatsächliche Regeln.
Anthony Pegram
1
@AnthonyPegram Sätze sind ungeordnet - Frob kann manchmal 3. sein, manchmal 2. sein. Sie können sich nicht darauf verlassen und eine Schleife (oder ein Sprachfeature wie das von Python in) erforderlich machen, wenn der Test "Frob wurde erfolgreich zu einer vorhandenen Sammlung hinzugefügt" lautet.
Izkata
1
@ Izbata, seine Frage erwähnt ausdrücklich , dass die Reihenfolge wichtig ist. Seine Worte: "Andere werden auf einen Wert gesetzt, der von ihrer Position in der Sammlung abhängt." Es gibt viele Auflistungstypen in C # (die Sprache, auf die er verweist), deren Einfügung sortiert ist. In diesem Fall können Sie sich auch auf die Reihenfolge der Listen in Python verlassen, einer Sprache, die Sie erwähnen.
Anthony Pegram
Angenommen, Sie testen eine Reset-Methode für eine Sammlung. Sie müssen die Sammlung durchlaufen und jedes Element überprüfen. Abhängig von der Größe der Sammlung ist es lächerlich, sie nicht in einer Schleife zu testen. Oder nehmen wir an, ich teste etwas, das jedes Element in einer Sammlung erhöhen soll. Sie können alle Elemente auf den gleichen Wert einstellen, Ihr Inkrement aufrufen und dann überprüfen. Dieser Test ist zum Kotzen. Sie sollten mehrere von ihnen auf unterschiedliche Werte einstellen, increment aufrufen und überprüfen, ob alle unterschiedlichen Werte korrekt inkrementiert wurden. Wenn Sie nur einen zufälligen Artikel in der Sammlung markieren, überlässt dies viel dem Zufall.
Iheanyi
Ich werde nicht auf diese Weise antworten, weil ich eine Unmenge von Abstimmungen bekomme, sondern oft nur toString()die Sammlung und vergleiche mit dem, was es sein sollte. Einfach und funktioniert.
user949300

Antworten:

16

TL; DR:

  • Schreiben Sie den Test
  • Wenn der Test zu viel bewirkt, kann der Code auch zu viel bewirken.
  • Es ist möglicherweise kein Komponententest (aber kein schlechter Test).

Das erste, was getestet werden muss, ist, dass das Dogma nicht hilfreich ist. Ich lese gerne den Weg des Testivus, der auf unbeschwerte Art und Weise auf einige Probleme mit dem Dogma hinweist.

Schreiben Sie den Test, der geschrieben werden muss.

Wenn der Test auf irgendeine Weise geschrieben werden muss, schreiben Sie ihn auf diese Weise. Der Versuch, den Test in ein idealisiertes Testlayout zu zwingen oder gar nicht zu haben, ist keine gute Sache. Ein Test heute, der es testet, ist besser als ein "perfekter" Test eines späteren Tages.

Ich werde auch auf den hässlichen Test hinweisen:

Wenn der Code hässlich ist, können die Tests hässlich sein.

Sie schreiben nicht gerne hässliche Tests, aber hässlicher Code muss am meisten getestet werden.

Lassen Sie sich nicht von hässlichem Code davon abhalten, Tests zu schreiben, sondern von hässlichem Code, mehr davon zu schreiben.

Dies kann als Binsenweisheit für diejenigen angesehen werden, die schon lange folgen ... und sie werden nur in der Art des Denkens und Schreibens von Tests verankert. Für Leute, die noch nicht an diesen Punkt gekommen sind und dies versuchen, können Erinnerungen hilfreich sein (ich finde sogar, dass ein erneutes Lesen mir hilft, mich nicht auf ein Dogma einzulassen).


Bedenken Sie beim Schreiben eines hässlichen Tests, dass der Code möglicherweise darauf hinweist, dass er zu viel zu tun versucht. Wenn der zu testende Code zu komplex ist, um durch Schreiben eines einfachen Tests ordnungsgemäß ausgeführt zu werden, können Sie den Code in kleinere Teile aufteilen, die mit den einfacheren Tests getestet werden können. Man sollte nicht einen Unit - Test schreiben, der alles tut (es kann nicht sein , Einheit Test dann). So wie "Gottobjekte" schlecht sind, sind auch "Gotteinheitentests" schlecht und sollten Hinweise sein, den Code noch einmal anzusehen.

Sie sollten in der Lage sein, den gesamten Code durch solche einfachen Tests mit angemessener Abdeckung auszuüben. Tests, die mehr End-to-End-Tests durchführen, die sich mit größeren Fragen befassen ("Ich habe dieses Objekt in XML zusammengefasst, über die Regeln an den Webdienst gesendet, zurückgesetzt und nicht zusammengefasst"), sind ein ausgezeichneter Test - aber sicher nicht ist kein Komponententest (und fällt in den Bereich der Integrationstests - auch wenn er Dienste verspottet hat, die er aufruft und die für den Test in Speicherdatenbanken benutzerdefiniert sind). Möglicherweise wird das XUnit-Framework weiterhin zum Testen verwendet, das Testing-Framework macht es jedoch nicht zu einem Komponententest.


quelle
7

Ich füge eine neue Antwort hinzu, weil meine Perspektive sich von der unterscheidet, als ich die ursprüngliche Frage und Antwort geschrieben habe. es macht keinen Sinn, sie zu einer zusammenzufassen.

Ich sagte in der ursprünglichen Frage

An meinem Design kann ich jedoch nichts Falsches erkennen. (Wie sonst können Sie eine Liste von Datenobjekten erstellen, von deren Werten einige in der Reihenfolge abhängen, in der sie sich befinden.)

Hier habe ich mich geirrt. Nachdem ich im letzten Jahr die Funktionsprogrammierung durchgeführt hatte, wurde mir klar, dass ich nur eine Sammeloperation mit einem Akku benötigte. Dann könnte ich meine Funktion als reine Funktion schreiben, die auf eine Sache abzielt, und eine Standardbibliotheksfunktion verwenden, um sie auf die Sammlung anzuwenden.

Meine neue Antwort lautet also: Verwende funktionale Programmiertechniken und du wirst dieses Problem die meiste Zeit ganz vermeiden. Sie können Ihre Funktionen schreiben, um einzelne Dinge zu bearbeiten und sie erst im letzten Moment auf Sammlungen von Dingen anzuwenden. Wenn sie jedoch rein sind, können Sie sie ohne Bezugnahme auf Sammlungen testen.

Verlassen Sie sich für komplexere Logik auf eigenschaftsbasierte Tests . Wenn sie über eine Logik verfügen, sollte diese kleiner sein als die Logik des getesteten Codes und umgekehrt, und bei jedem Test wird so viel mehr überprüft als bei einem fallbasierten Komponententest, dass sich die geringe Menge an Logik lohnt.

Vor allem stützen Sie sich immer auf Ihre Typen . Holen Sie sich die stärksten Typen, die Sie können, und nutzen Sie sie zu Ihrem Vorteil. Dies reduziert die Anzahl der Tests, die Sie zuerst schreiben müssen.

Kazark
quelle
4

Versuchen Sie nicht, zu viele Dinge gleichzeitig zu testen. Jede der Eigenschaften jedes Datenobjekts in der Auflistung ist zu viel für einen Test. Stattdessen empfehle ich:

  1. Wenn die Sammlung eine feste Länge hat, schreiben Sie einen Einheitentest, um die Länge zu validieren. Wenn die Länge variabel ist, schreiben Sie mehrere Tests auf Längen, die das Verhalten charakterisieren (z. B. 0, 1, 3, 10). In beiden Fällen sollten Sie die Eigenschaften in diesen Tests nicht validieren.
  2. Schreiben Sie einen Komponententest, um jede der Eigenschaften zu validieren. Wenn die Auflistung eine feste Länge und eine kurze Länge hat, müssen Sie für jeden Test nur eine Eigenschaft jedes Elements angeben. Wenn es eine feste Länge, aber eine lange Länge hat, wählen Sie eine repräsentative, aber kleine Stichprobe der Elemente aus, um für jede Eigenschaft eine Aussage zu machen. Wenn es sich um eine variable Länge handelt, generieren Sie eine relativ kurze, aber repräsentative Sammlung (dh möglicherweise drei Elemente) und behaupten Sie gegen jeweils eine Eigenschaft.

Auf diese Weise werden die Tests so klein, dass das Weglassen von Schleifen nicht schmerzhaft erscheint. C # / Unit-Beispiel, gegebene Testmethode ICollection<Foo> generateFoos(uint numberOfFoos):

[Test]
void generate_zero_foos_returns_empty_list() { ... }
void generate_one_foo_returns_list_of_one_foo() { ... }
void generate_three_foos_returns_list_of_three_foos() { ... }
void generated_foos_have_sequential_ID()
{
    var foos = generateFoos(3).GetEnumerable();
    foos.MoveNext();
    Assert.AreEqual("ID1", foos.Current.id);
    foos.MoveNext();
    Assert.AreEqual("ID2", foos.Current.id);
    foos.MoveNext();
    Assert.AreEqual("ID3", foos.Current.id);
}
void generated_foos_have_bar()
{
    var foos = generateFoos(3).GetEnumerable();
    foos.MoveNext();
    Assert.AreEqual("baz", foos.Current.bar);
    foos.MoveNext();
    Assert.AreEqual("baz", foos.Current.bar);
    foos.MoveNext();
    Assert.AreEqual("baz", foos.Current.bar);
}

Wenn Sie an das "Flat Unit Test" -Paradigma (keine verschachtelten Strukturen / Logik) gewöhnt sind, scheinen diese Tests ziemlich sauber zu sein. Somit wird Logik in den Tests vermieden, indem das ursprüngliche Problem als der Versuch erkannt wird, zu viele Eigenschaften gleichzeitig zu testen, anstatt dass Schleifen fehlen.

Kazark
quelle
1
Osherove würde Ihren Kopf auf eine Platte legen, wenn Sie 3 Asserts hätten. ;) Der erste Fehler bedeutet, dass Sie den Rest nie validieren. Beachten Sie auch , dass Sie nicht wirklich vermeiden die Schleife. Sie haben es nur explizit in seine ausgeführte Form erweitert. Nicht eine harte Kritik, sondern nur ein Vorschlag etwas mehr Praxis zu isolieren Ihre Testfälle auf den Mindestbetrag möglich zu bekommen, dir mehr zu geben spezifisches Feedback , wenn etwas schiefgeht, während sie weiterhin andere Fälle zu überprüfen , die möglicherweise immer noch (oder nicht passieren könnten, mit ihre eigenen spezifischen Rückmeldungen).
Anthony Pegram
3
@AnthonyPegram Ich kenne das One-Assert-Per-Test-Paradigma. Ich bevorzuge das Mantra "test one thing" (wie von Bob Martin gegenüber "one-assert-per-test" in Clean Code vertreten ). Randnotiz: Unit-Testing-Frameworks, die "erwarten" und "behaupten", sind nett (Google Tests). Warum teilen Sie Ihre Vorschläge im Übrigen nicht in eine vollständige Antwort mit Beispielen auf? Ich denke, ich könnte davon profitieren.
Kazark