Was passiert mit den Tests von Methoden, wenn diese Methode nach einer Neugestaltung in TDD privat wird?

29

Angenommen, ich beginne mit der Entwicklung eines Rollenspiels mit Charakteren, die andere Charaktere und dergleichen angreifen.

Mit TDD mache ich einige Testfälle, um die Logik in der Character.receiveAttack(Int)Methode zu testen . Etwas wie das:

@Test
fun healthIsReducedWhenCharacterIsAttacked() {
    val c = Character(100) //arg is the health
    c.receiveAttack(50) //arg is the suffered attack damage
    assertThat(c.health, is(50));
}

Angenommen, ich habe 10 Methoden Testmethode receiveAttack. Jetzt füge ich eine Methode hinzu Character.attack(Character)(die receiveAttackMethode aufruft ) und nach einigen TDD-Zyklen, die sie testen, treffe ich eine Entscheidung: Character.receiveAttack(Int)sollte sein private.

Was passiert mit den letzten 10 Testfällen? Soll ich sie löschen? Soll ich die Methode beibehalten public(glaube ich nicht)?

Bei dieser Frage geht es nicht darum, wie private Methoden getestet werden, sondern wie mit ihnen nach einer Neugestaltung bei der Anwendung von TDD umgegangen wird

Tyrannisieren
quelle
2
Mögliches Duplikat des Testens privater Methoden als geschützt
Gnat
10
Wenn es privat ist, testen Sie es nicht, es ist so einfach. Entfernen Sie und machen Sie den Refactor Dance
Kayess
6
Ich gehe hier wahrscheinlich gegen den Strich. Aber ich vermeide generell private Methoden um jeden Preis. Ich bevorzuge mehr Tests als weniger Tests. Ich weiß, was die Leute denken "Was, also haben Sie nie irgendeine Funktionalität, die Sie dem Verbraucher nicht zugänglich machen möchten?". Ja, ich habe viele, die ich nicht preisgeben möchte. Wenn ich eine private Methode habe, baue ich sie stattdessen in eine eigene Klasse um und verwende diese Klasse aus der ursprünglichen Klasse. Die neue Klasse kann als internaloder in der entsprechenden Sprache markiert werden , um zu verhindern, dass sie angezeigt wird. In der Tat ist Kevin Clines Antwort diese Art von Ansatz.
User9993
3
@ user9993 du scheinst es rückwärts zu haben. Wenn es für Sie wichtig ist, mehr Tests durchzuführen, können Sie nur durch eine Abdeckungsanalyse sicherstellen, dass Sie nichts Wichtiges verpasst haben. Und für Coverage-Tools spielt es keine Rolle, ob die Methode privat oder öffentlich ist oder was auch immer. Hoffnung , dass Dinge öffentlich zu machen irgendwie für die fehlende Deckungsanalyse zu kompensieren gibt ein falsches Gefühl der Sicherheit Ich habe Angst
gnat
2
@gnat Aber ich habe nie etwas über "keine Berichterstattung" gesagt? Mein Kommentar zu "Ich bevorzuge mehr Tests als weniger Tests" hätte das deutlich machen sollen. Ich bin mir nicht sicher, worauf du genau hinaus willst. Natürlich teste ich auch den Code, den ich extrahiert habe. Das ist der springende Punkt.
User9993

Antworten:

52

In TDD dienen die Tests als ausführbare Dokumentation Ihres Designs. Ihr Design hat sich geändert, also muss natürlich auch Ihre Dokumentation!

Beachten Sie, dass die attackMethode in TDD nur als Ergebnis eines fehlgeschlagenen Testdurchlaufs hätte angezeigt werden können. Was bedeutet, attackwird von einem anderen Test getestet. Das bedeutet, dass indirekt receiveAttack durch attackdie Tests abgedeckt wird . Im Idealfall sollte jede Änderung receiveAttackmindestens einen der attackTests unterbrechen.

Und wenn nicht, dann gibt es Funktionen receiveAttack, die nicht mehr benötigt werden und nicht mehr existieren sollten!

Da dies receiveAttackbereits getestet wurde attack, spielt es keine Rolle, ob Sie Ihre Tests behalten oder nicht. Wenn Ihr Test - Framework macht es einfach , private Methoden zu testen, und wenn Sie sich entscheiden , private Methoden zu testen, dann können Sie sie halten. Sie können sie jedoch auch löschen, ohne die Testabdeckung und das Vertrauen zu verlieren.

Jörg W. Mittag
quelle
14
Dies ist eine gute Antwort, außer für "Wenn Ihr Test-Framework das Testen privater Methoden vereinfacht und Sie sich zum Testen privater Methoden entschließen, können Sie diese beibehalten." Private Methoden sind Implementierungsdetails und sollten niemals direkt getestet werden.
David Arno
20
@DavidArno: Ich bin anderer Meinung, aus diesem Grund sollten die Interna eines Moduls niemals getestet werden. Die Interna eines Moduls können jedoch sehr komplex sein, weshalb Unit-Tests für jede einzelne interne Funktionalität von Nutzen sein können. Unit-Tests werden verwendet, um die Invarianten einer Funktion zu überprüfen. Wenn eine private Methode Invarianten (Vor- / Nachbedingungen) hat, kann ein Unit-Test wertvoll sein.
Matthieu M.
8
"Aus diesem Grund sollten die Interna eines Moduls niemals getestet werden ". Diese Interna sollten niemals direkt getestet werden. Bei allen Tests sollten nur öffentliche APIs getestet werden. Wenn ein internes Element über eine öffentliche API nicht erreichbar ist, löschen Sie es, da es nichts tut.
David Arno
28
@DavidArno Wenn Sie nach dieser Logik eine ausführbare Datei (und keine Bibliothek) erstellen, sollten Sie überhaupt keine Komponententests durchführen. - "Funktionsaufrufe sind nicht Teil der öffentlichen API! Nur Befehlszeilenargumente! Wenn eine interne Funktion Ihres Programms über ein Befehlszeilenargument nicht erreichbar ist, löschen Sie sie, da sie nichts bewirkt." - Private Funktionen gehören zwar nicht zur öffentlichen API der Klasse, aber zur internen API der Klasse. Und während Sie nicht unbedingt die interne API einer Klasse testen müssen, können Sie mit derselben Logik die interne API einer ausführbaren Datei testen.
RM
7
@RM, wenn ich eine ausführbare Datei nicht modular erstellen würde, wäre ich gezwungen, zwischen spröden Tests der Interna oder nur Integrationstests mit der ausführbaren und der Laufzeit-E / A zu wählen. Daher würde ich es nach meiner eigentlichen Logik und nicht nach Ihrer Strawman-Version modular erstellen (z. B. über eine Reihe von Bibliotheken). Die öffentlichen APIs dieser Module können dann auf eine nicht spröde Weise getestet werden.
David Arno
23

Wenn die Methode so komplex ist, dass sie getestet werden muss, sollte sie in einer Klasse öffentlich sein. So refactor Sie aus:

public class X {
  private int complexity(...) {
    ...
  }
  public void somethingElse() {
    int c = complexity(...);
  }
}

zu:

public class Complexity {
  public int calculate(...) {
    ...
  }
}

public class X {
  private Complexity complexity;
  public X(Complexity complexity) { // dependency injection happiness
    this.complexity = complexity;
  }

  public void something() {
    int c = complexity.calculate(...);
  }
}

Verschieben Sie den aktuellen Test für X.complexity nach ComplexityTest. Dann schreiben Sie X. Etwas, indem Sie sich über die Komplexität lustig machen.

Nach meiner Erfahrung bringt die Umgestaltung auf kleinere Klassen und kürzere Methoden enorme Vorteile. Sie sind leichter zu verstehen, leichter zu testen und werden am Ende mehr als erwartet wiederverwendet.

Kevin Cline
quelle
Ihre Antwort erklärt die Idee, die ich in meinem Kommentar zur Frage von OP zu erklären versuchte, viel klarer. Gute Antwort.
User9993
3
Danke für deine Antwort. Die Methode receiveAttack ist eigentlich ganz einfach ( this.health = this.health - attackDamage). Vielleicht ist es für diesen Moment eine überentwickelte Lösung, es in eine andere Klasse zu extrahieren.
Héctor
1
Dies ist definitiv ein Overkill für den OP - er möchte in den Laden fahren, nicht zum Mond fliegen - aber im Allgemeinen eine gute Lösung.
Wenn die Funktion so einfach ist, ist es vielleicht eine Überentwicklung, dass sie überhaupt als Funktion definiert ist.
David K
1
Heute mag es übertrieben sein, aber in 6 Monaten, wenn es eine Menge Änderungen an diesem Code gibt, werden die Vorteile klar sein. Und in einer anständigen IDE sollte das Extrahieren von Code in eine separate Klasse heutzutage mit ein paar Tastenanschlägen höchstens eine überentwickelte Lösung sein, wenn man bedenkt, dass in der Laufzeit-Binärdatei sowieso alles auf dasselbe hinausläuft.
Stephen Byrne
6

Angenommen, ich habe 10 Methoden, die die Methode "receiveAttack" testen. Jetzt füge ich eine Methode Character.attack (Character) hinzu (die die Methode receiveAttack aufruft), und nach einigen TDD-Zyklen, in denen ich sie teste, treffe ich eine Entscheidung: Character.receiveAttack (Int) sollte privat sein.

Beachten Sie hierbei, dass Sie eine Methode aus der API entfernen müssen . Die Höflichkeit der Abwärtskompatibilität würde vorschlagen

  1. Wenn Sie es nicht entfernen müssen, belassen Sie es in der API
  2. Wenn Sie es nicht entfernen müssen noch , markieren Sie es dann als veraltet und wenn möglich Dokument , wenn das Ende des Lebens geschehen wird
  3. Wenn Sie es entfernen müssen, liegt eine größere Versionsänderung vor

Die Tests werden entfernt bzw. ersetzt, wenn Ihre API die Methode nicht mehr unterstützt. Zu diesem Zeitpunkt ist die private Methode ein Implementierungsdetail, das Sie überarbeiten können sollten.

An diesem Punkt befinden Sie sich wieder in der Standardfrage, ob Ihre Testsuite direkt auf Implementierungen zugreifen soll, statt nur über die öffentliche API zu interagieren. Eine private Methode sollte ersetzt werden können, ohne dass die Testsuite stört . Daher würde ich davon ausgehen, dass das Testpaar aufhört - entweder in den Ruhestand zu gehen oder mit der Implementierung auf eine separat testbare Komponente umzusteigen.

VoiceOfUnreason
quelle
3
Verfall ist nicht immer ein Problem. Aus der Frage: "Nehmen wir an, ich beginne mit der Entwicklung ..." Wenn die Software noch nicht veröffentlicht wurde, ist die Ablehnung kein Problem. Außerdem bedeutet "ein Rollenspiel", dass es sich nicht um eine wiederverwendbare Bibliothek handelt, sondern um Binärsoftware, die sich an Endbenutzer richtet. Während einige Endbenutzer-Software eine öffentliche API haben (z. B. MS Office), haben die meisten keine. Auch die Software , die funktioniert nur eine öffentliche API hat einen Teil davon für Plugins, Skripte (zB Spiele mit LUA - Erweiterung) ausgesetzt ist , oder andere Funktionen. Es lohnt sich jedoch, die Idee für den allgemeinen Fall, den das OP beschreibt, zur Sprache zu bringen.