Wie schreibt man Unit-Tests vor dem Refactoring?

55

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();
   }
}
PDStat
quelle
14
Ist es wirklich Refactoring Sie tun planen oder neu zu gestalten ? Weil die Antwort in beiden Fällen unterschiedlich sein kann.
Herby
4
Ich arbeite an der Definition "Mit Refactoring ändern Sie per Definition nicht, was Ihre Software tut, sondern wie sie es tut." Ich glaube also, dass es sich in diesem Fall um ein Refactoring handelt. Sie können mein Verständnis des Begriffs
PDStat,
21
Schreiben Sie keine Integrationstests. Das von Ihnen geplante "Refactoring" liegt über der Ebene der Komponententests. Testen Sie nur die neuen Klassen (oder die alten, von denen Sie wissen, dass Sie sie behalten).
Hören Sie auf, Monica
2
Definiert Ihre Software in Bezug auf die Definition von Refactoring klar, was sie tut? Mit anderen Worten, ist es bereits in Module mit unabhängigen APIs "eingeteilt"? Wenn nicht, können Sie es nicht umgestalten, außer vielleicht auf der höchsten (dem Benutzer zugewandten) Ebene. Auf Modulebene werden Sie es unweigerlich neu gestalten. Verschwenden Sie in diesem Fall nicht Ihre Zeit damit, Komponententests zu schreiben, bevor Sie über Einheiten verfügen.
Kevin Krumwiede
4
Es ist sehr wahrscheinlich, dass Sie ein wenig überarbeiten müssen, ohne das Sicherheitsnetz der Tests zu haben, um es in ein Testgeschirr zu bekommen. Der beste Rat, den ich Ihnen geben kann, ist, dass Sie es nicht von Hand tun sollten, wenn Ihre IDE oder Ihr Refactoring-Tool dies nicht für Sie tun. Wenden Sie so lange automatisierte Refactorings an, bis Sie den CUT in ein Gurtzeug einbauen können. Sie werden auf jeden Fall eine Kopie von Michael Feders "Effektiv mit Legacy-Code arbeiten" abholen wollen.
RubberDuck

Antworten:

56

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.

Brian Agnew
quelle
1
Das heißt, Sie führen wahrscheinlich eher Integrations- oder Systemtests als Komponententests durch. Sie werden wahrscheinlich immer noch ein "Unit Testing" -Tool dafür verwenden, aber Sie werden mit jedem Test mehr als eine Codeeinheit treffen.
17.
Ja. Das ist sehr viel der Fall. Ihr Regressionstest könnte auch etwas tut sehr hohe Niveau zB REST - Anfragen an einen Server und möglicherweise einen nachfolgenden Datenbank - Test (dh definitiv kein Gerät Test!)
Brian Agnew
40

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.

Michael Borgwardt
quelle
+1 für Integrationstests. Abhängig von der App können Sie möglicherweise mit dem eigentlichen Senden von Anforderungen an die Web-App beginnen. Was die App zurücksendet, sollte sich nicht nur aufgrund des Refactorings ändern, auch wenn es HTML zurücksendet, ist dies sicherlich weniger testbar.
jpmc26
Ich mag die Phrase "Pin-down" Tests.
Brian Agnew
12

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 .

[..] Das Problem ist meines Erachtens, dass beim Refactor diese Tests unterbrochen werden, wenn sich die Logik ändert und die Tests unter Berücksichtigung der vorherigen Struktur geschrieben werden (verspottete Abhängigkeiten usw.). [ ..]

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).

Daniel Jour
quelle
1
Diese. Die Tests werden schrecklich aussehen , aber sie decken das vorhandene Verhalten ab. Wenn der Code überarbeitet wird, werden die Tests im Sperrschritt ausgeführt. Wiederholen, bis Sie etwas haben, auf das Sie stolz sind. ++
RubberDuck
1
Ich schließe mich diesen beiden Buchempfehlungen an - ich habe immer beide zur Hand, wenn ich mich mit testlosem Code beschäftigen muss.
Toby Speight
5

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:

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 so aus:

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

Verspotten Sie die Abhängigkeiten von DocumentDao, anstatt sie zu verspotten:

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

Jetzt können Sie die Logik von MyDocumentServicenach verschieben, DocumentDaoohne dass die Tests unterbrochen werden. Die Tests zeigen, dass die Funktionalität identisch ist (sofern Sie sie getestet haben).

Winston Ewert
quelle
Wenn Sie DocumentService testen und das DAO nicht verspotten, handelt es sich überhaupt nicht um einen Komponententest. Es ist etwas zwischen Einheits- und Integrationstest. Oder nicht?
Laiv
7
@Laiv, es gibt tatsächlich erhebliche Unterschiede in der Art und Weise, wie Menschen den Begriff Unit-Test verwenden. Einige meinen damit nur streng isolierte Tests. Andere enthalten alle Tests, die schnell ausgeführt werden. Einige enthalten alles, was in einem Testframework ausgeführt wird. Letztendlich spielt es jedoch keine Rolle, wie Sie den Begriff Unit Test definieren möchten. Die Frage ist, welche Tests nützlich sind, damit wir uns nicht davon ablenken lassen, wie genau wir Unit-Tests definieren.
Winston Ewert
Hervorragender Punkt, der zeigt, dass Nützlichkeit das Wichtigste ist. Extravagante Komponententests für die einfachsten Algorithmen, nur um Komponententests durchzuführen, schaden mehr als nützen, wenn nicht nur eine massive Verschwendung von Zeit und wertvollen Ressourcen. Dies kann auf so ziemlich alles angewendet werden und ist etwas, von dem ich mir wünschte, ich hätte es früher in meiner Karriere gewusst.
Lee
3

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.

Encaitar
quelle
3

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 .

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. 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

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 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.

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.

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.

Laiv
quelle
3

tl; dr Schreiben Sie nicht Unit - Tests. Schreiben Sie Tests auf einer angemesseneren Ebene.


In Anbetracht Ihrer Arbeitsdefinition von Refactoring:

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

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:

Welches wesentliche, öffentlich sichtbare Verhalten wird die Anwendung sowohl vor als auch nach dem Refactoring aufweisen? Wie kann ich testen, ob das Ding noch genauso funktioniert?

Paul Draper
quelle
2

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.

topo morto
quelle
Immer an Gründen für Abstimmungen interessiert!
topo morto
1

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:

[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 getestete Code hat eine schlechte API. Ich kann es ohne Änderung der Tests umgestalten, 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 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.

default.kramer
quelle