Ich habe einige Antworten auf ähnliche Fragen wie "Wie halten Sie Ihre Komponententests beim Refactoring aufrecht?" Gelesen. In meinem Fall ist das Szenario insofern etwas anders, als ich ein Projekt erhalten habe, um es zu überprüfen und mit einigen Standards in Einklang zu bringen, die wir haben. Derzeit gibt es überhaupt keine Tests für das Projekt!
Ich habe eine Reihe von Dingen identifiziert, von denen ich denke, dass sie besser gemacht werden könnten, z. B. DAO-Typcode NICHT in einer Service-Schicht zu mischen.
Vor dem Refactoring schien es eine gute Idee zu sein, Tests für den vorhandenen Code zu schreiben. Das Problem ist meines Erachtens, dass beim Refactor diese Tests brechen, wenn ich die Logik ändere und die Tests unter Berücksichtigung der vorherigen Struktur geschrieben werden (verspottete Abhängigkeiten usw.).
Was wäre in meinem Fall die beste Vorgehensweise? Ich bin versucht, die Tests um den überarbeiteten Code herum zu schreiben, aber ich bin mir bewusst, dass das Risiko besteht, dass ich Dinge falsch überarbeite, die das gewünschte Verhalten ändern könnten.
Unabhängig davon, ob dies ein Refactor oder ein Redesign ist, bin ich froh, dass mein Verständnis dieser Begriffe korrigiert wird. Derzeit arbeite ich an der folgenden Definition für das Refactoring: "Beim Refactoring ändern Sie per Definition nicht, was Ihre Software tut. Sie ändern, wie es das macht. Ich ändere also nicht, was die Software macht. Ich ändere, wie / wo sie es macht.
Ebenso kann ich das Argument sehen, dass, wenn ich die Signatur von Methoden ändere, dies als Neugestaltung betrachtet werden könnte.
Hier ist ein kurzes Beispiel
MyDocumentService.java
(Strom)
public class MyDocumentService {
...
public List<Document> findAllDocuments() {
DataResultSet rs = documentDAO.findAllDocuments();
List<Document> documents = new ArrayList<>();
for(DataObject do: rs.getRows()) {
//get row data create new document add it to
//documents list
}
return documents;
}
}
MyDocumentService.java
(überarbeitet / neu gestaltet, was auch immer)
public class MyDocumentService {
...
public List<Document> findAllDocuments() {
//Code dealing with DataResultSet moved back up to DAO
//DAO now returns a List<Document> instead of a DataResultSet
return documentDAO.findAllDocuments();
}
}
quelle
Antworten:
Sie suchen nach Tests, die auf Regressionen prüfen . dh ein vorhandenes Verhalten zu brechen. Ich würde zunächst herausfinden, auf welcher Ebene dieses Verhalten gleich bleibt und welche Schnittstelle dieses Verhalten steuert, und dann anfangen, Tests durchzuführen.
Sie haben jetzt einige Tests, die bestätigen, dass Ihr Verhalten das gleiche bleibt , was auch immer Sie unterhalb dieses Niveaus tun .
Sie haben völlig Recht zu hinterfragen, wie die Tests und der Code synchron bleiben können. Wenn Ihre Schnittstelle zu einer Komponente dieselbe bleibt, können Sie einen Test erstellen und für beide Implementierungen (beim Erstellen der neuen Implementierung) dieselben Bedingungen festlegen. Wenn dies nicht der Fall ist, müssen Sie akzeptieren, dass ein Test für eine redundante Komponente ein redundanter Test ist.
quelle
Die empfohlene Vorgehensweise besteht darin, zunächst "Pin-down-Tests" zu schreiben, die das aktuelle Verhalten des Codes testen, möglicherweise auch Fehler, ohne dass Sie sich in den Wahnsinn begeben müssen, zu erkennen, ob ein bestimmtes Verhalten, das gegen Anforderungsdokumente verstößt, ein Fehler ist. Problemumgehung für etwas, das Sie nicht kennen oder das eine undokumentierte Änderung der Anforderungen darstellt.
Am sinnvollsten ist es, wenn diese Pin-down-Tests auf einem hohen Niveau durchgeführt werden, dh Integration statt Komponententests, damit sie bei Beginn der Umgestaltung weiter funktionieren.
Möglicherweise sind jedoch einige Umgestaltungen erforderlich, um den Code testbar zu machen. Achten Sie nur darauf, dass Sie sich an "sichere" Umgestaltungen halten. Zum Beispiel können in fast allen Fällen private Methoden öffentlich gemacht werden, ohne etwas zu beschädigen.
quelle
Ich schlage vor, - falls noch nicht geschehen - sowohl das effektive Arbeiten mit Legacy-Code als auch das Umgestalten zu lesen - das Design des vorhandenen Codes zu verbessern .
Ich sehe nicht unbedingt das als Problem: Schreiben Sie die Tests, die Struktur des Codes ändern, und dann die Teststruktur anpassen auch . Auf diese Weise erhalten Sie direktes Feedback, ob Ihre neue Struktur tatsächlich besser ist als die alte, da die angepassten Tests in diesem Fall einfacher zu schreiben sind (und das Ändern der Tests daher relativ einfach sein sollte, was das Risiko einer Neueinführung verringert Fehler bestehen die Tests).
Auch, wie andere bereits geschrieben haben: Schreiben Sie keine zu detaillierten Tests (zumindest nicht am Anfang). Versuchen Sie, auf einem hohen Abstraktionsniveau zu bleiben (daher werden Ihre Tests wahrscheinlich besser als Regressionstests oder sogar Integrationstests charakterisiert).
quelle
Schreiben Sie keine strengen Komponententests, in denen Sie alle Abhängigkeiten verspotten. Einige Leute werden Ihnen sagen, dass dies keine echten Unit-Tests sind. Ignoriere sie. Diese Tests sind nützlich, und darauf kommt es an.
Schauen wir uns Ihr Beispiel an:
Ihr Test sieht wahrscheinlich so aus:
Verspotten Sie die Abhängigkeiten von DocumentDao, anstatt sie zu verspotten:
Jetzt können Sie die Logik von
MyDocumentService
nach verschieben,DocumentDao
ohne dass die Tests unterbrochen werden. Die Tests zeigen, dass die Funktionalität identisch ist (sofern Sie sie getestet haben).quelle
Wie Sie sagen, wenn Sie das Verhalten ändern, handelt es sich um eine Transformation und nicht um einen Refactor. Auf welcher Ebene Sie das Verhalten ändern, macht den Unterschied aus.
Wenn es keine formalen Tests auf höchster Ebene gibt, versuchen Sie, eine Reihe von Anforderungen zu finden, die Clients (die den Code aufrufen oder Menschen) erfüllen müssen, damit Ihr Code funktioniert. Dies ist die Liste der Testfälle, die Sie implementieren müssen.
Zur Beantwortung Ihrer Frage zu Änderungen an Implementierungen, die Änderungen an Testfällen erfordern, empfehlen wir Ihnen einen Blick auf Detroit (klassisch) und London (verspottet) TDD. Martin Fowler spricht darüber in seinem großartigen Artikel Mocks sind keine Stubs, aber viele Menschen haben Meinungen. Wenn Sie auf der höchsten Ebene beginnen, auf der sich Ihre externen Komponenten nicht ändern können, und sich nach unten arbeiten, sollten die Anforderungen relativ stabil bleiben, bis Sie eine Ebene erreichen, die sich wirklich ändern muss.
Ohne Tests wird dies schwierig, und Sie sollten in Betracht ziehen, Clients über doppelte Codepfade auszuführen (und die Unterschiede aufzuzeichnen), bis Sie sicher sind, dass Ihr neuer Code genau das tut, was er tun muss.
quelle
Hier mein Ansatz. Es ist zeitaufwändig, da es sich um einen Refactor-Test in 4 Phasen handelt.
Was ich herausstellen werde, passt möglicherweise besser zu Komponenten mit höherer Komplexität als die im Beispiel der Frage herausgestellten.
Auf jeden Fall gilt die Strategie für alle Komponentenkandidaten, die über eine Schnittstelle (DAO, Services, Controller, ...) normalisiert werden sollen.
1. Die Schnittstelle
Sammeln Sie alle öffentlichen Methoden von MyDocumentService und fügen Sie sie in einer Schnittstelle zusammen. Zum Beispiel. Wenn es bereits existiert, verwenden Sie dieses, anstatt eines neuen zu setzen .
Dann zwingen wir MyDocumentService , diese neue Schnittstelle zu implementieren.
So weit, ist es gut. Es wurden keine wesentlichen Änderungen vorgenommen, wir haben den aktuellen Vertrag eingehalten und das Verhalten bleibt unberührt.
2. Komponententest des Legacy-Codes
Hier haben wir die harte Arbeit. So richten Sie eine Testsuite ein Wir sollten so viele Fälle wie möglich festlegen: erfolgreiche Fälle und auch Fehlerfälle. Letztere dienen der Qualität des Ergebnisses.
Anstatt MyDocumentService zu testen, verwenden wir jetzt die Schnittstelle als zu testenden Vertrag.
Ich werde nicht auf Details eingehen, also vergib mir, wenn mein Code zu einfach oder zu agnostisch aussieht
Diese Phase dauert länger als jede andere in diesem Ansatz. Und es ist das wichtigste, weil es den Bezugspunkt für zukünftige Vergleiche setzt.
Hinweis: Da keine wesentlichen Änderungen vorgenommen wurden, bleibt das Verhalten unberührt. Ich schlage vor, hier einen Tag in das SCM einzufügen. Tag oder Zweig spielt keine Rolle. Mach einfach eine Version.
Wir wollen es für Rollbacks, Versionsvergleiche und möglicherweise für die parallele Ausführung des alten und des neuen Codes.
3. Refactoring
Refactor wird in eine neue Komponente implementiert. Wir werden keine Änderungen am bestehenden Code vornehmen. Der erste Schritt ist so einfach wie das Kopieren und Einfügen von MyDocumentService und das Umbenennen in CustomDocumentService (zum Beispiel).
Neue Klasse implementiert weiterhin DocumentService . Dann gehen Sie und refactorize getAllDocuments () . (Fangen wir mit eins an. Pin-Refactors)
Möglicherweise müssen einige Änderungen an der DAO-Schnittstelle / den DAO-Methoden vorgenommen werden. Wenn ja, ändern Sie den vorhandenen Code nicht. Implementieren Sie Ihre eigene Methode in der DAO-Schnittstelle. Kommentieren Sie den alten Code mit " Veraltet" und Sie werden später wissen, was entfernt werden sollte.
Es ist wichtig, die vorhandene Implementierung nicht zu beschädigen oder zu ändern. Wir wollen beide Dienste parallel ausführen und dann die Ergebnisse vergleichen.
4. Aktualisieren von DocumentServiceTestSuite
Ok, jetzt der einfachere Teil. Hinzufügen der Tests der neuen Komponente.
Jetzt haben wir oldResult und newResult beide unabhängig validiert, aber wir können auch miteinander vergleichen. Diese letzte Validierung ist optional und vom Ergebnis abhängig. Vielleicht ist es nicht vergleichbar.
Es mag nicht zu viel Sinn machen, zwei Sammlungen auf diese Weise zu vergleichen, aber es wäre für jede andere Art von Objekt gültig (Pojos, Datenmodellentitäten, DTOs, Wrapper, native Typen ...)
Anmerkungen
Ich würde es nicht wagen zu sagen, wie man Unit-Tests macht oder wie man Schein-Bibliotheken benutzt. Ich traue mich auch nicht zu sagen, wie Sie den Refactor machen müssen. Ich wollte eine globale Strategie vorschlagen. Wie Sie vorankommen, hängt von Ihnen ab. Sie wissen genau, wie Code ist, seine Komplexität und ob eine solche Strategie einen Versuch wert ist. Fakten wie Zeit und Ressourcen sind hier wichtig. Auch darauf kommt es an, was Sie in Zukunft von diesen Tests erwarten.
Ich habe meine Beispiele durch einen Dienst gestartet und würde mit DAO und so weiter folgen. Ein tiefer Einblick in die Abhängigkeitsstufen. Mehr oder weniger könnte man es als Up-Bottom- Strategie bezeichnen. Bei geringfügigen Änderungen / Umgestaltungen ( wie im Tour-Beispiel ) würde ein Bottom-up- Ansatz die Aufgabe erleichtern. Weil der Umfang der Änderungen gering ist.
Schließlich liegt es an Ihnen, veralteten Code zu entfernen und alte Abhängigkeiten auf die neuen umzuleiten.
Entfernen Sie auch veraltete Tests und die Arbeit ist erledigt. Wenn Sie die alte Lösung mit ihren Tests versioniert haben, können Sie sie jederzeit überprüfen und miteinander vergleichen.
Infolge so vieler Arbeiten haben Sie Legacy-Code getestet, validiert und versioniert. Und neuer Code, getestet, validiert und bereit zur Versionierung.
quelle
tl; dr Schreiben Sie nicht Unit - Tests. Schreiben Sie Tests auf einer angemesseneren Ebene.
In Anbetracht Ihrer Arbeitsdefinition von Refactoring:
es gibt ein sehr breites spektrum. An einem Ende steht eine eigenständige Änderung einer bestimmten Methode, möglicherweise unter Verwendung eines effizienteren Algorithmus. Auf der anderen Seite wird in eine andere Sprache portiert.
Unabhängig von der Stufe der Umgestaltung / Neugestaltung ist es wichtig, dass Tests auf dieser Stufe oder höher durchgeführt werden.
Automatisierte Tests werden häufig nach Level klassifiziert als:
Unit Tests - Einzelkomponenten (Klassen, Methoden)
Integrationstests - Interaktionen zwischen Komponenten
Systemtests - Die komplette Anwendung
Schreiben Sie die Teststufe auf, die das Refactoring im Wesentlichen unberührt überstehen kann.
Denken:
quelle
Verschwenden Sie keine Zeit damit, Tests zu schreiben, die an Stellen eingesetzt werden, an denen Sie davon ausgehen können, dass sich die Benutzeroberfläche auf nicht triviale Weise ändert. Dies ist oft ein Zeichen dafür, dass Sie versuchen, Klassen zu testen, die von Natur aus „kollaborativ“ sind - deren Wert nicht darin besteht, was sie selbst tun, sondern darin, wie sie mit einer Reihe eng verwandter Klassen interagieren, um wertvolles Verhalten hervorzurufen . Es ist dieses Verhalten, das Sie testen möchten, was bedeutet, dass Sie auf einer höheren Ebene testen möchten. Tests unterhalb dieses Niveaus erfordern oft hässliche Verspottungen, und die daraus resultierenden Tests können die Entwicklung eher in Mitleidenschaft ziehen als das Verhalten verteidigen.
Lassen Sie sich nicht zu sehr darüber aufregen, ob Sie einen Refactor, ein Redesign oder was auch immer machen. Sie können Änderungen vornehmen, die auf der unteren Ebene eine Neugestaltung mehrerer Komponenten darstellen, auf einer höheren Integrationsstufe jedoch lediglich einen Refaktor darstellen. Der Punkt ist, klar zu machen, welches Verhalten für Sie von Wert ist, und dieses Verhalten zu verteidigen, während Sie gehen.
Es kann hilfreich sein, beim Verfassen Ihrer Tests zu berücksichtigen. Kann ich einer Qualitätssicherung, einem Produktbesitzer oder einem Benutzer leicht beschreiben, was dieser Test tatsächlich testet? Wenn es so aussieht, als ob die Beschreibung des Tests zu esoterisch und technisch wäre, testen Sie möglicherweise auf der falschen Ebene. Testen Sie an den Punkten / Ebenen, die "Sinn ergeben", und verfälschen Sie Ihren Code nicht mit Tests auf allen Ebenen.
quelle
Ihre erste Aufgabe ist es, die "ideale Methodensignatur" für Ihre Tests zu finden. Bemühen Sie sich, es zu einer reinen Funktion zu machen . Dies sollte unabhängig von dem Code sein, der gerade getestet wird. Es ist eine kleine Adapterschicht. Schreiben Sie Ihren Code in diese Adapterschicht. Wenn Sie jetzt Ihren Code überarbeiten, müssen Sie nur noch die Adapterebene ändern. Hier ist ein einfaches Beispiel:
Die Tests sind gut, aber der getestete Code hat eine schlechte API. Ich kann es ohne Änderung der Tests umgestalten, indem ich einfach meine Adapterschicht aktualisiere:
Dieses Beispiel ist nach dem Don't Repeat Yourself-Prinzip ziemlich naheliegend, in anderen Fällen ist es jedoch möglicherweise nicht so naheliegend. Der Vorteil geht über DRY hinaus - der eigentliche Vorteil ist die Entkopplung der Tests vom zu testenden Code.
Natürlich ist diese Technik möglicherweise nicht in allen Situationen ratsam. Es gibt beispielsweise keinen Grund, Adapter für POCOs / POJOs zu schreiben, da diese nicht wirklich über eine API verfügen, die sich unabhängig vom Testcode ändern könnte. Auch wenn Sie eine kleine Anzahl von Tests schreiben, würde eine relativ große Adapterschicht wahrscheinlich Aufwand verschwenden.
quelle