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?
quelle
Antworten:
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.
quelle
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:
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.
quelle
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:
Ä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.
quelle
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).
quelle