it-swarm.com.de

Wie schreibe ich Unit-Tests vor dem Refactoring?

Ich habe einige Antworten auf ähnliche Fragen gelesen, z. B. "Wie sorgen Sie dafür, dass Ihre Unit-Tests beim Refactoring funktionieren?". In meinem Fall ist das Szenario insofern etwas anders, als ich ein Projekt zur Überprüfung und Anpassung an einige unserer Standards erhalten habe. Derzeit gibt es überhaupt keine Tests für das Projekt!

Ich habe eine Reihe von Dingen identifiziert, von denen ich denke, dass sie besser hätten gemacht werden können, z. B. das NICHT-Mischen von DAO-Typcode in einer Service-Schicht.

Vor dem Refactoring schien es eine gute Idee zu sein, Tests für den vorhandenen Code zu schreiben. Das Problem scheint mir zu sein, dass beim Refactor diese Tests unterbrochen werden, wenn ich ändere, wo bestimmte Logik ausgeführt wird, und die Tests unter Berücksichtigung der vorherigen Struktur geschrieben werden (verspottete Abhängigkeiten usw.).

Was wäre in meinem Fall der beste Weg, um vorzugehen? 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 umgestalte, die das gewünschte Verhalten ändern könnten.

Unabhängig davon, ob es sich um einen Refactor oder ein Redesign handelt, bin ich froh, dass mein Verständnis dieser Begriffe korrigiert wurde. 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 geht. ". Ich ändere also nicht, was die Software macht. Ich würde ändern, wie/wo sie es macht.

Ebenso kann ich das Argument sehen, dass, wenn ich die Signatur von Methoden ändere, dies als Neugestaltung angesehen 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 (was auch immer überarbeitet/neu gestaltet)

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();
   }
}
55
PDStat

Sie suchen nach Tests, die nach Regressionen suchen. d.h. ein bestehendes Verhalten brechen. Ich würde zunächst herausfinden, auf welcher Ebene dieses Verhalten gleich bleibt und auf welcher Schnittstelle dieses Verhalten gleich bleibt, und an diesem Punkt Tests durchführen.

Sie haben jetzt einige Tests, die bestätigen, dass alles, was Sie tun nten auf dieser Ebene, Ihr Verhalten gleich bleibt.

Sie haben zu Recht die Frage, wie die Tests und der Code synchron bleiben können. Wenn Ihr Schnittstelle für eine Komponente gleich bleibt, können Sie einen Test darum schreiben und die gleichen Bedingungen für beide Implementierungen festlegen (während Sie die neue Implementierung erstellen). Wenn dies nicht der Fall ist, müssen Sie akzeptieren, dass ein Test für eine redundante Komponente ein redundanter Test ist.

56
Brian Agnew

Die empfohlene Vorgehensweise besteht darin, zunächst "Pin-down-Tests" zu schreiben, die das aktuelle Verhalten des Codes testen, möglicherweise einschließlich Fehler, ohne dass Sie in den Wahnsinn abtauchen 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.

Es ist am sinnvollsten, wenn diese Pin-down-Tests auf einem hohen Niveau sind, d. H. Integration statt Unit-Tests, damit sie weiter funktionieren, wenn Sie mit dem Refactoring beginnen.

Es können jedoch einige Refactorings erforderlich sein, um den Code testbar zu machen. Achten Sie jedoch darauf, dass Sie sich an "sichere" Refactorings halten. Zum Beispiel können in fast allen Fällen private Methoden veröffentlicht werden, ohne etwas zu beschädigen.

40

Ich schlage vor, - falls Sie dies noch nicht getan haben - sowohl Effektiv mit Legacy-Code arbeiten als auch Refactoring - Verbesserung des Designs vorhandenen Codes zu lesen.

[..] Das Problem scheint mir zu sein, dass beim Refactor diese Tests unterbrochen werden, wenn ich ändere, wo bestimmte Logik ausgeführt wird, und die Tests unter Berücksichtigung der vorherigen Struktur geschrieben werden (verspottete Abhängigkeiten usw.) [ ..]

Ich sehe dies nicht unbedingt als Problem: Schreiben Sie die Tests, ändern Sie die Struktur Ihres Codes und passen Sie dann auch die Teststruktur an . Dies gibt Ihnen direktes Feedback, ob Ihre neue Struktur tatsächlich besser ist als die alte, denn wenn dies der Fall ist, sind die angepassten Tests einfacher zu schreiben ( Das Ändern der Tests sollte daher relativ einfach sein und das Risiko verringern, dass ein neu eingeführter Fehler die Tests besteht.

Auch, wie andere bereits geschrieben haben: Schreiben Sie nicht auch detaillierte Tests (zumindest nicht am Anfang). Versuchen Sie, auf einem hohen Abstraktionsniveau zu bleiben (daher werden Ihre Tests wahrscheinlich besser als Regressions- oder sogar Integrationstests charakterisiert).

12
Daniel Jour

Schreiben Sie keine strengen Komponententests, bei denen Sie alle Abhängigkeiten verspotten. Einige Leute werden Ihnen sagen, dass dies keine echten Komponententests sind. Ignoriere sie. Diese Tests sind nützlich, und darauf kommt es an.

Schauen wir uns Ihr Beispiel an:

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;
   }
}

Ihr Test sieht wahrscheinlich ungefähr so ​​aus:

DocumentDao documentDao = Mock.create(DocumentDao.class);
Mock.when(documentDao.findAllDocuments())
    .thenReturn(DataResultSet.create(...))
assertEquals(..., new MyDocumentService(documentDao).findAllDocuments());

Anstatt DocumentDao zu verspotten, verspotten Sie seine Abhängigkeiten:

DocumentDao documentDao = new DocumentDao(db);
Mock.when(db...)
    .thenReturn(...)
assertEquals(..., new MyDocumentService(documentDao).findAllDocuments());

Jetzt können Sie die Logik von MyDocumentService nach DocumentDao verschieben, ohne dass die Tests unterbrochen werden. Die Tests zeigen, dass die Funktionalität dieselbe ist (soweit Sie sie getestet haben).

5
Winston Ewert

tl; dr Schreibe keine Unit-Tests. Schreiben Sie Tests auf einem angemesseneren Niveau.


In Anbetracht Ihrer Arbeitsdefinition von Refactoring:

sie ändern nicht, was Ihre Software tut, Sie ändern, wie sie es tut

es gibt sehr breites Spektrum. An einem Ende steht eine in sich geschlossene Änderung einer bestimmten Methode, möglicherweise unter Verwendung eines effizienteren Algorithmus. Am anderen Ende wird in eine andere Sprache portiert.

Unabhängig davon, auf welcher Ebene Refactoring/Redesign durchgeführt wird, ist es wichtig, dass Tests auf dieser oder einer höheren Ebene durchgeführt werden.

Automatisierte Tests werden häufig nach Ebenen klassifiziert als:

  • nit Tests - Einzelne Komponenten (Klassen, Methoden)

  • Integrationstests - Interaktionen zwischen Komponenten

  • Systemtests - Die vollständige Anwendung

Schreiben Sie die Teststufe auf, die das Refactoring im Wesentlichen unberührt aushalten kann.

Überlegen:

Welches wesentliche, öffentlich sichtbare Verhalten hat die Anwendung sowohl vor als auch nach das Refactoring? Wie kann ich testen, dass das Ding immer noch genauso funktioniert?

3
Paul Draper

Wie Sie sagen, wenn Sie das Verhalten ändern, ist es eine Transformation und kein Refaktor. Auf welcher Ebene Sie das Verhalten ändern, macht den Unterschied.

Wenn es keine formalen Tests auf höchster Ebene gibt, versuchen Sie, eine Reihe von Anforderungen zu finden, die Clients (aufrufender Code oder Menschen) nach Ihrer Neugestaltung gleich bleiben müssen, damit Ihr Code als funktionsfähig gilt. Dies ist die Liste der Testfälle, die Sie implementieren müssen.

Um Ihre Frage zu ändernden Implementierungen zu beantworten, die das Ändern von Testfällen erfordern, würde ich vorschlagen, dass Sie sich Detroit (klassisch) gegen London (Mockist) TDD ansehen. Martin Fowler spricht darüber in seinem großartigen Artikel Mocks sind keine Stubs aber viele Leute haben Meinungen. Wenn Sie auf der höchsten Ebene beginnen, auf der sich Ihre externen Daten nicht ändern können, und sich nach unten arbeiten, sollten die Anforderungen ziemlich 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 zwei Codepfade auszuführen (und die Unterschiede aufzuzeichnen), bis Sie sicher sein können, dass Ihr neuer Code genau das tut, was er tun muss.

3
Encaitar

Hier mein Ansatz. Es kostet Zeit, da es sich um einen Refaktortest in 4 Phasen handelt.

Was ich herausstellen werde, passt möglicherweise besser zu Komponenten mit einer höheren Komplexität als die im Beispiel der Frage dargestellte.

Auf jeden Fall gilt die Strategie für alle Komponentenkandidaten, die von einer Schnittstelle (DAO, Services, Controller, ...) normalisiert werden sollen.

1. Die Schnittstelle

Sammeln wir alle öffentlichen Methoden aus MyDocumentService und fügen wir sie alle zu einer Schnittstelle zusammen. Zum Beispiel. Wenn es bereits existiert, benutze dieses, anstatt ein neues zu setzen.

public interface DocumentService {

   List<Document> getAllDocuments();

   //more methods here...
}

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.

public class MyDocumentService implements DocumentService {

 @Override
 public List<Document> getAllDocuments(){
         //legacy code here as it is.
        // with no changes ...
  }
}

2. nit-Test 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

public class DocumentServiceTestSuite {

   @Mock
   MyDependencyA mockDepA;

   @Mock
   MyDependencyB mockDepB;

    //... More mocks

   DocumentService service;

  @Before
   public void initService(){
       service = MyDocumentService(mockDepA, mockDepB);
      //this is purposed way to inject 
      //dependencies. Replace it with one you like more.  
   }

   @Test
   public void getAllDocumentsOK(){
         // here I mock depA and depB
         // wanted behaivors...

         List<Document> result = service.getAllDocuments();

          Assert.assertX(result);
          Assert.assertY(result);
           //... As many you think appropiate
    } 
 }

Diese Phase dauert bei diesem Ansatz länger als jede andere. Und es ist das Wichtigste, weil es den Bezugspunkt für zukünftige Vergleiche darstellt.

Hinweis: Da keine wesentlichen Änderungen vorgenommen wurden und das Verhalten unberührt bleibt. Ich schlage vor, hier ein Tag im SCM zu machen. Tag oder Zweig spielen keine Rolle. Mach einfach eine Version.

Wir wollen es für Rollbacks, Versionsvergleiche und möglicherweise für parallele Ausführungen des alten und des neuen Codes.

3. Refactoring

Refactor wird in eine neue Komponente implementiert. Wir werden keine Änderungen am vorhandenen 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 weiter DocumentService. Dann gehen Sie und refactorize getAllDocuments (). (Beginnen wir mit eins. Pin-Refaktoren)

Möglicherweise sind einige Änderungen an der Schnittstelle/den Methoden des DAO erforderlich. Wenn ja, ändern Sie den vorhandenen Code nicht. Implementieren Sie Ihre eigene Methode in der DAO-Schnittstelle. Kommentieren Sie den alten Code als Veraltet und Sie werden später wissen, was entfernt werden sollte.

Es ist wichtig, die vorhandene Implementierung nicht zu unterbrechen oder zu ändern. Wir wollen beide services parallel ausführen und dann die Ergebnisse vergleichen.

public class CustomDocumentService implements DocumentService {

 @Override
 public List<Document> getAllDocuments(){
         //new code here ...
         //due to im refactoring service 
         //I do the less changes possible on its dependencies (DAO).
         //these changes will come later 
         //and they will have their own tests
  }
 }

4. Aktualisieren von DocumentServiceTestSuite

Ok, jetzt der einfachere Teil. Hinzufügen der Tests der neuen Komponente.

public class DocumentServiceTestSuite {

   @Mock
   MyDependencyA mockDepA;

   @Mock
   MyDependencyB mockDepB;

   DocumentService service;
   DocumentService customService;

  @Before
   public void initService(){
       service = MyDocumentService(mockDepA, mockDepB);
        customService = CustomDocumentService(mockDepA, mockDepB);
       // this is purposed way to inject 
       //dependencies. Replace it with the one you like more
   }

   @Test
   public void getAllDocumentsOK(){
         // here I mock depA and depB
         // wanted behaivors...

         List<Document> oldResult = service.getAllDocuments();

          Assert.assertX(oldResult);
          Assert.assertY(oldResult);
           //... As many you think appropiate

          List<Document> newResult = customService.getAllDocuments();

          Assert.assertX(newResult);
          Assert.assertY(newResult);
           //... The very same made to oldResult

          //this is optional
Assert.assertEquals(oldResult,newResult);
    } 
 }

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.

Möglicherweise nicht zu viel gesehen, um zwei Sammlungen auf diese Weise zu vergleichen, wäre aber für jede andere Art von Objekt gültig (Pojos, Datenmodellentitäten, DTOs, Wrapper, native Typen ...).

Notizen

Ich würde es nicht wagen zu sagen, wie man Unit-Tests macht oder wie man Scheinbibliotheken verwendet. Ich wage es auch nicht zu sagen, wie Sie den Refactor machen müssen. Ich wollte eine globale Strategie vorschlagen. Wie Sie es vorantreiben, hängt von Ihnen ab. Sie wissen genau, wie Code ist, wie komplex er ist und ob eine solche Strategie einen Versuch wert ist. Fakten wie Zeit und Ressourcen sind hier wichtig. Auch ist wichtig, was Sie in Zukunft von diesen Tests erwarten.

Ich habe meine Beispiele durch einen Dienst begonnen und würde mit DAO und so weiter folgen. Tief in die Abhängigkeitsebenen eintauchen. Mehr oder weniger könnte es als oben-unten Strategie beschrieben werden. Bei geringfügigen Änderungen/Refaktoren ( wie im Tourbeispiel) würde ein von unten nach oben die Aufgabe jedoch einfacher erledigen. Weil der Umfang der Änderungen gering ist.

Schließlich liegt es an Ihnen, veralteten Code zu entfernen und alte Abhängigkeiten auf den 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 vergleichen.

Infolge so vieler Arbeiten haben Sie Legacy-Code getestet, validiert und versioniert. Und neuer Code, getestet, validiert und bereit zur Versionierung.

3
Laiv

Verschwenden Sie keine Zeit damit, Tests zu schreiben, die sich an Stellen einfügen, an denen Sie davon ausgehen können, dass sich die Benutzeroberfläche auf nicht triviale Weise ändern wird. Dies ist oft ein Zeichen dafür, dass Sie versuchen, Klassen zu testen, die "kollaborativer" Natur sind - deren Wert nicht darin liegt, was sie selbst tun, sondern darin, wie sie mit einer Reihe eng verwandter Klassen interagieren, um wertvolles Verhalten zu erzeugen . Es ist das Verhalten, das Sie testen möchten, was bedeutet, dass Sie auf einer höheren Ebene testen möchten. Tests unterhalb dieses Niveaus erfordern oft viel hässliches Verspotten, und die daraus resultierenden Tests können die Entwicklung eher beeinträchtigen als die Verteidigung des Verhaltens unterstützen.

Lassen Sie sich nicht zu sehr darauf ein, ob Sie einen Refactor, ein Redesign oder was auch immer machen. Sie können Änderungen vornehmen, die auf der unteren Ebene eine Neugestaltung einer Reihe von Komponenten darstellen, auf einer höheren Integrationsebene jedoch einfach einen Refactor darstellen. Es geht darum, klar zu machen, welches Verhalten für Sie von Wert ist, und dieses Verhalten zu verteidigen, während Sie gehen.

Es kann nützlich sein, beim Schreiben Ihrer Tests zu berücksichtigen, ob dieser Test einem QS, einem Produktbesitzer oder einem Benutzer leicht beschreiben kann, was dieser Test tatsächlich testet. Wenn es so aussieht, als wäre es zu esoterisch und technisch, den Test zu beschreiben, testen Sie möglicherweise auf der falschen Ebene. Testen Sie an den Punkten/Ebenen, die "Sinn machen", und verputzen Sie Ihren Code nicht mit Tests auf jeder Ebene.

Ihre erste Aufgabe besteht darin, die "ideale Methodensignatur" für Ihre Tests zu finden. Bemühen Sie sich, daraus eine reine Funktion zu machen. Dies sollte unabhängig von dem Code sein, der tatsächlich getestet wird. Es ist eine kleine Adapterschicht. Schreiben Sie Ihren Code in diese Adapterschicht. Wenn Sie jetzt Ihren Code umgestalten, müssen Sie nur noch die Adapterschicht ändern. Hier ist ein einfaches Beispiel:

[TestMethod]
public void simple_addition()
{
    Assert.AreEqual(7, Eval("3 + 4"));
}

[TestMethod]
public void order_of_operations()
{
    Assert.AreEqual(52, Eval("2 + 5 * 10"));
}

[TestMethod]
public void absolute_value()
{
    Assert.AreEqual(9, Eval("abs(-9)"));
    Assert.AreEqual(5, Eval("abs(5)"));
    Assert.AreEqual(0, Eval("abs(0)"));
}

static object Eval(string expression)
{
    // This is the code under test.
    // I can refactor this as much as I want without changing the tests.
    var settings = new EvaluatorSettings();
    Evaluator.Settings = settings;
    Evaluator.Evaluate(expression);
    return Evaluator.LastResult;
}

Die Tests sind gut, aber der zu testende Code hat eine schlechte API. Ich kann es umgestalten, ohne die Tests zu ändern, indem ich einfach meine Adapterschicht aktualisiere:

static object Eval(string expression)
{
    // After refactoring...
    var settings = new EvaluatorSettings();
    var evaluator = new Evaluator(settings);
    return evaluator.Evaluate(expression);
}

Dieses Beispiel scheint nach dem Prinzip "Nicht wiederholen" ziemlich offensichtlich zu sein, in anderen Fällen ist es jedoch möglicherweise nicht so offensichtlich. Der Vorteil geht über DRY - der eigentliche Vorteil ist die Entkopplung der Tests vom zu testenden Code.

Natürlich ist diese Technik möglicherweise nicht in allen Situationen ratsam. Beispielsweise gibt es keinen Grund, Adapter für POCOs/POJOs zu schreiben, da diese nicht über eine API verfügen, die sich unabhängig vom Testcode ändern könnte. Auch wenn Sie eine kleine Anzahl von Tests schreiben, wäre eine relativ große Adapterschicht wahrscheinlich eine Verschwendung von Aufwand.

1
default.kramer