Wie halten Sie Ihre Tests während der Neugestaltung effizient am Laufen?

14

Eine gut getestete Codebasis hat eine Reihe von Vorteilen, aber das Testen bestimmter Aspekte des Systems führt zu einer Codebasis, die gegenüber einigen Arten von Änderungen resistent ist.

Ein Beispiel ist das Testen auf bestimmte Ausgaben - z. B. Text oder HTML. Tests werden oft (naiv?) Geschrieben, um einen bestimmten Textblock als Ausgabe für einige Eingabeparameter zu erwarten oder um nach bestimmten Abschnitten in einem Block zu suchen.

Wenn Sie das Verhalten des Codes ändern, um neue Anforderungen zu erfüllen oder weil sich durch Usability-Tests die Benutzeroberfläche geändert hat, müssen Sie auch die Tests ändern - möglicherweise sogar Tests, bei denen es sich nicht speziell um Komponententests für den zu ändernden Code handelt.

  • Wie schaffen Sie es, diese Tests zu finden und neu zu schreiben? Was ist, wenn Sie nicht einfach "alle ausführen und sie vom Framework aussortieren lassen" können?

  • Welche anderen Arten von zu testendem Code führen zu gewöhnlich fragilen Tests?

Alex Feinman
quelle
Inwiefern unterscheidet sich das erheblich von programmers.stackexchange.com/questions/5898/… ?
AShelly
4
Diese irrtümlich gestellte Frage zum Refactoring - Unit-Tests sollten beim Refactoring unveränderlich sein.
Alex Feinman

Antworten:

9

Ich weiß, dass die TDD-Leute diese Antwort hassen werden, aber ein großer Teil davon ist für mich, sorgfältig zu wählen, wo etwas getestet werden soll.

Wenn ich mit Unit-Tests in den unteren Ebenen zu verrückt werde, kann keine sinnvolle Änderung vorgenommen werden, ohne die Unit-Tests zu ändern. Wenn die Benutzeroberfläche niemals offengelegt wird und nicht für die Wiederverwendung außerhalb der App vorgesehen ist, ist dies nur ein unnötiger Aufwand für Änderungen, die ansonsten schnell vorgenommen worden wären.

Umgekehrt ist jeder Test, den Sie ändern möchten, ein Beweis für etwas, das Sie möglicherweise an anderer Stelle brechen, wenn er offengelegt oder wiederverwendet wird.

In einigen Projekten kann dies bedeuten, dass Sie Ihre Tests von der Akzeptanzstufe abwärts als von den Einheitentests aufwärts entwerfen. und mit weniger Unit-Tests und mehr Integrationstests.

Dies bedeutet nicht, dass Sie ein einzelnes Merkmal und einen einzelnen Code erst dann identifizieren können, wenn dieses Merkmal die Akzeptanzkriterien erfüllt. Es bedeutet einfach, dass Sie in einigen Fällen die Akzeptanzkriterien nicht mit Unit-Tests messen.

Rechnung
quelle
Ich denke, Sie wollten "außerhalb des Moduls" schreiben, nicht "außerhalb der App".
SamB
SamB, es kommt darauf an. Wenn die Schnittstelle an einigen Stellen mit einer App intern, aber nicht öffentlich ist, würde ich in Betracht ziehen, sie auf einer höheren Ebene zu testen, wenn ich der Meinung bin, dass die Schnittstelle wahrscheinlich flüchtig ist.
Bill
Ich habe festgestellt, dass dieser Ansatz mit TDD sehr kompatibel ist. Ich beginne gerne in den oberen Schichten der Anwendung, die näher am Endbenutzer liegen, damit ich die unteren Schichten entwerfen kann und weiß, wie die oberen Schichten die unteren Schichten verwenden müssen. Wenn Sie im Wesentlichen von oben nach unten bauen, können Sie die Schnittstelle zwischen zwei Ebenen genauer entwerfen.
Greg Burghardt
4

Ich habe gerade meinen SIP-Stack grundlegend überarbeitet und den gesamten TCP-Transport neu geschrieben. (Dies war im Vergleich zu den meisten Refactorings ein Near-Refactor in größerem Maßstab.)

Kurz gesagt, es gibt eine TIdSipTcpTransport-Unterklasse von TIdSipTransport. Alle TIdSipTransports nutzen eine gemeinsame Testsuite. Innerhalb von TIdSipTcpTransport befanden sich eine Reihe von Klassen - eine Zuordnung, die Verbindungs- / Initiierungsnachrichtenpaare, TCP-Clients mit Threading, einen TCP-Server mit Threading usw. enthielt.

Folgendes habe ich getan:

  • Löschte die Klassen, die ich ersetzen wollte.
  • Die Testsuiten für diese Klassen wurden gelöscht.
  • Links die Testsuite spezifisch TIdSipTcpTransport (und es war immer noch die Testsuite , die für alle TIdSipTransports).
  • Führen Sie die Tests TIdSipTransport / TIdSipTcpTransport aus, um sicherzustellen, dass alle fehlgeschlagen sind.
  • Auskommentierter Test für alle bis auf einen TIdSipTransport / TIdSipTcpTransport.
  • Wenn ich eine Klasse hinzufügen musste, fügte ich ihr Schreibtests hinzu, um so viele Funktionen aufzubauen, dass der einzige unkommentierte Test bestanden wurde.
  • Aufschäumen, ausspülen, wiederholen.

Ich wusste also, was ich noch tun musste, in Form der auskommentierten Tests (*), und wusste, dass der neue Code dank der neuen Tests, die ich geschrieben hatte, erwartungsgemäß funktionierte.

(*) Wirklich, Sie müssen sie nicht auskommentieren. Lass sie einfach nicht laufen. 100 nicht bestandene Tests sind nicht sehr ermutigend. Außerdem bedeutet das Kompilieren weniger Tests in meinem speziellen Setup eine schnellere Test-Schreib-Refaktor-Schleife.

Frank Shearar
quelle
Ich habe das schon vor einigen Monaten gemacht und es hat bei mir ganz gut funktioniert. Diese Methode konnte ich jedoch nicht unbedingt anwenden, wenn ich mich mit einem Kollegen bei der grundlegenden Neugestaltung unseres Domain-Modell-Moduls zusammenschloss (was wiederum die Neugestaltung aller anderen Module im Projekt auslöste).
Marco Ciambrone
3

Wenn Tests fragil sind, finde ich es normalerweise, weil ich das Falsche teste. Nehmen wir zum Beispiel die HTML-Ausgabe. Wenn Sie die tatsächliche HTML-Ausgabe überprüfen, ist Ihr Test anfällig. Sie sind jedoch nicht an der tatsächlichen Ausgabe interessiert, sondern daran, ob die Informationen übermittelt werden, die sie enthalten sollen. Leider erfordert dies Aussagen über den Inhalt des Gehirns des Benutzers und kann daher nicht automatisch erfolgen.

Du kannst:

  • Generieren Sie den HTML-Code als Rauchtest, um sicherzustellen, dass er tatsächlich ausgeführt wird
  • Verwenden Sie ein Vorlagensystem, damit Sie den Vorlagenprozessor und die an die Vorlage gesendeten Daten testen können, ohne die genaue Vorlage selbst zu testen.

Ähnliches passiert mit SQL. Wenn Sie die tatsächliche SQL angeben, versuchen Ihre Klassen, Sie in Schwierigkeiten zu bringen. Sie wollen wirklich die Ergebnisse behaupten. Daher verwende ich während meiner Unit-Tests eine SQLITE-Speicherdatenbank, um sicherzustellen, dass mein SQL tatsächlich das tut, was es soll.

Winston Ewert
quelle
Es kann auch hilfreich sein, strukturelles HTML zu verwenden.
SamB
@ SamB sicherlich würde das helfen, aber ich glaube nicht, dass es das Problem vollständig lösen wird
Winston Ewert
Natürlich nicht, nichts kann :-)
SamB
-1

Erstellen Sie zunächst eine NEUE API, die genau das tut, was Sie für Ihr NEUES API-Verhalten wünschen. Wenn diese neue API den gleichen Namen wie eine ältere API hat, füge ich den Namen _NEW an den neuen API-Namen an.

int DoSomethingInterestingAPI ();

wird:

int DoSomethingInterestingAPI_NEW (int takes_more_arguments); int DoSomethingInterestingAPI_OLD (); int DoSomethingInterestingAPI () {DoSomethingInterestingAPI_NEW (whatever_default_mimics_the_old_API); OK, zu diesem Zeitpunkt sind alle Ihre Regressionstests noch nicht bestanden. Verwenden Sie dazu den Namen DoSomethingInterestingAPI ().

NEXT, gehen Sie Ihren Code durch und ändern Sie alle Aufrufe von DoSomethingInterestingAPI () in die entsprechende Variante von DoSomethingInterestingAPI_NEW (). Dies beinhaltet das Aktualisieren / Umschreiben aller Teile Ihrer Regressionstests, die geändert werden müssen, um die neue API zu verwenden.

Als nächstes markieren Sie DoSomethingInterestingAPI_OLD () als [[veraltet ()]. Behalten Sie die veraltete API so lange bei, wie Sie möchten (bis Sie den gesamten Code, der möglicherweise davon abhängt, sicher aktualisiert haben).

Bei diesem Ansatz sind alle Fehler in Ihren Regressionstests einfach Fehler in diesem Regressionstest oder identifizieren Fehler in Ihrem Code - genau so, wie Sie es möchten. Dieser schrittweise Prozess der Überarbeitung einer API durch explizites Erstellen von _NEW- und _OLD-Versionen der API ermöglicht es Ihnen, Teile des neuen und alten Codes für eine Weile nebeneinander zu haben.

Hier ist ein gutes (hartes) Beispiel für diesen Ansatz in der Praxis. Ich hatte die Funktion BitSubstring () - wobei ich den Ansatz gewählt hatte, dass der dritte Parameter die Anzahl der Bits in der Teilzeichenfolge ist. Um mit anderen APIs und Mustern in C ++ konsistent zu sein, wollte ich wechseln, um als Argumente für die Funktion zu beginnen / zu enden.

https://github.com/SophistSolutions/Stroika/commit/003dd8707405c43e735ca71116c773b108c217c0

Ich habe eine Funktion BitSubstring_NEW mit der neuen API erstellt und meinen gesamten Code so aktualisiert, dass er diese Funktion verwendet (ohne weitere Aufrufe an BitSubString). Aber ich habe die Implementierung für einige Releases (Monate) verlassen - und als veraltet markiert -, damit jeder zu BitSubString_NEW wechseln kann (und zu diesem Zeitpunkt das Argument von einer Zählung in einen Start- / End-Stil ändern kann).

DANN - als dieser Übergang abgeschlossen war, habe ich ein weiteres Commit ausgeführt, indem ich BitSubString () gelöscht und BitSubString_NEW-> BitSubString () umbenannt habe (und den Namen BitSubString_NEW verworfen habe).

Lewis Pringle
quelle
Hängen Sie niemals Suffixe an, die keine Bedeutung haben oder für Namen selbstverachtend sind. Bemühen Sie sich immer, aussagekräftige Namen zu vergeben.
Basilevs
Sie haben den Punkt völlig verpasst. Erstens - das sind keine Suffixe, die "keine Bedeutung haben". Sie haben die Bedeutung, dass die API von einer älteren auf eine neuere übergeht. Tatsächlich ist das der springende Punkt der FRAGE, auf die ich geantwortet habe, und der springende Punkt der Antwort. Die Namen CLEARLY kommunizieren, welches die ALTE API ist, welches die NEUE API ist und welches der endgültige Zielname der API ist, sobald der Übergang abgeschlossen ist. UND - Die Suffixe _OLD / _NEW sind NUR während des API-Änderungsübergangs temporär.
Lewis Pringle
Viel Glück mit der NEW_NEW_3-Version der API drei Jahre später.
Basilevs