it-swarm.com.de

Ist es eine schlechte Praxis, Code ausschließlich zu Testzwecken zu ändern?

Ich habe eine Debatte mit einem Programmiererkollegen darüber, ob es eine gute oder eine schlechte Praxis ist, einen funktionierenden Code nur so zu ändern, dass er testbar ist (z. B. über Unit-Tests).

Meiner Meinung nach ist es in Ordnung, natürlich im Rahmen der Aufrechterhaltung guter objektorientierter und Software-Engineering-Praktiken (nicht "alles öffentlich machen" usw.).

Die Meinung meines Kollegen ist, dass das Ändern von Code (der funktioniert) nur zu Testzwecken falsch ist.

Stellen Sie sich als einfaches Beispiel diesen Code vor, der von einer Komponente verwendet wird (geschrieben in C #):

public void DoSomethingOnAllTypes()
{
    var types = Assembly.GetExecutingAssembly().GetTypes();

    foreach (var currentType in types)
    {
        // do something with this type (e.g: read it's attributes, process, etc).
    }
}

Ich habe vorgeschlagen, diesen Code zu ändern, um eine andere Methode aufzurufen, die die eigentliche Arbeit erledigt:

public void DoSomething(Assembly asm)
{
    // not relying on Assembly.GetExecutingAssembly() anymore...
}

Diese Methode verwendet ein Assembly-Objekt, an dem gearbeitet werden soll, sodass Sie Ihre eigene Assembly übergeben können, um die Tests durchzuführen. Mein Kollege hielt dies nicht für eine gute Praxis.

Was ist eine gute und gängige Praxis?

79
liortal

Das Ändern von Code, um ihn testbarer zu machen, bietet Vorteile, die über die Testbarkeit hinausgehen. Im Allgemeinen ist Code besser testbar

  • Ist leichter zu pflegen,
  • Ist leichter zu überlegen,
  • Ist lockerer gekoppelt und
  • Hat architektonisch ein besseres Gesamtdesign.
136
Robert Harvey

Es sind (scheinbar) gegnerische Kräfte im Spiel.

  • Einerseits möchten Sie die Kapselung erzwingen
  • Andererseits möchten Sie die Software testen können

Befürworter, alle "Implementierungsdetails" privat zu halten, sind normalerweise durch den Wunsch motiviert, die Kapselung aufrechtzuerhalten. Es ist jedoch ein missverstanden Ansatz zur Kapselung, alles gesperrt und nicht verfügbar zu halten. Wenn es das ultimative Ziel wäre, alles nicht verfügbar zu halten, wäre der einzig wahre gekapselte Code folgender:

static void Main(string[] args)

Schlägt Ihr Kollege vor, dies zum einzigen Zugangspunkt in Ihrem Code zu machen? Sollte der gesamte andere Code für externe Anrufer nicht zugänglich sein?

Kaum. Was macht es dann in Ordnung, einige Methoden öffentlich zu machen? Ist es nicht am Ende eine subjektive Designentscheidung?

Nicht ganz. Was Programmierer selbst auf unbewusster Ebene anleitet, ist wiederum das Konzept der Kapselung. Sie fühlen sich sicher, wenn Sie eine öffentliche Methode verfügbar machen, wenn sie ihre Invarianten ordnungsgemäß schützt .

Ich möchte keine private Methode verfügbar machen, die ihre Invarianten nicht schützt, aber oft können Sie sie so ändern, dass sie tut ihre Invarianten schützt und dann der Öffentlichkeit zugänglich machen (mit TDD machen Sie es natürlich umgekehrt).

Das Öffnen einer API für Testbarkeit ist eine gute Sache , denn was Sie wirklich tun, ist das Open/Closed-Prinzip anzuwenden .

Wenn Sie nur einen einzigen Aufrufer Ihrer API haben, wissen Sie nicht, wie flexibel Ihre API wirklich ist. Die Chancen stehen gut, dass es ziemlich unflexibel ist. Tests fungieren als zweiter Client, der Ihnen wertvolles Feedback zur Flexibilität Ihrer API gibt .

Wenn Tests darauf hindeuten, dass Sie Ihre API öffnen sollten, tun Sie dies. Behalten Sie jedoch die Kapselung bei, nicht indem Sie die Komplexität verbergen, sondern indem Sie die Komplexität auf ausfallsichere Weise offenlegen.

59
Mark Seemann

Sieht so aus, als würden Sie über Abhängigkeitsinjektion sprechen. Es ist wirklich üblich und IMO, ziemlich notwendig für die Testbarkeit.

Um die umfassendere Frage zu beantworten, ob es eine gute Idee ist, Code zu ändern, um ihn testbar zu machen, stellen Sie sich Folgendes vor: Code hat mehrere Verantwortlichkeiten, einschließlich a) Ausführen, b) Lesen durch Menschen und c) Zu getestet werden. Alle drei sind wichtig, und wenn Ihr Code nicht alle drei Verantwortlichkeiten erfüllt, würde ich sagen, dass es kein sehr guter Code ist. Also weg modifizieren!

21
Jason Swett

Es ist ein kleines Henne-Ei-Problem.

Einer der Hauptgründe, warum es gut ist, eine gute Testabdeckung Ihres Codes zu haben, ist, dass Sie damit furchtlos umgestalten können. Sie befinden sich jedoch in einer Situation, in der Sie den Code umgestalten müssen, um eine gute Testabdeckung zu erhalten! Und Ihr Kollege hat Angst.

Ich sehe den Standpunkt Ihres Kollegen. Sie haben Code, der (vermutlich) funktioniert, und wenn Sie ihn - aus welchem ​​Grund auch immer - umgestalten, besteht das Risiko, dass Sie ihn beschädigen.

Wenn es sich jedoch um Code handelt, für den eine laufende Wartung und Änderung erwartet wird, besteht dieses Risiko jedes Mal, wenn Sie daran arbeiten. Durch Refactoring jetzt und eine gewisse Testabdeckung jetzt können Sie dieses Risiko unter kontrollierten Bedingungen eingehen und den Code für zukünftige Änderungen in eine bessere Form bringen.

Daher würde ich sagen, dass Sie eine gute technische Praxis wünschen, es sei denn, diese bestimmte Codebasis ist ziemlich statisch und es wird nicht erwartet, dass in Zukunft bedeutende Arbeiten ausgeführt werden.

Natürlich, ob es gut ist Geschäft Übung ist eine ganze 'andere Dose Würmer ..

13
Carson63000

Dies mag nur ein Unterschied in der Betonung zu den anderen Antworten sein, aber ich würde sagen, dass der Code nicht überarbeitet werden sollte streng, um die Testbarkeit zu verbessern. Testbarkeit ist für die Wartung sehr wichtig, aber Testbarkeit ist nicht Selbstzweck. Daher würde ich ein solches Refactoring verschieben, bis Sie vorhersagen können, dass dieser Code gewartet werden muss, um das Geschäftsende voranzutreiben.

An dem Punkt, an dem Sie feststellen, dass dieser Code einige Wartungsarbeiten erfordert, wäre das ein guter Zeitpunkt, um die Testbarkeit zu überarbeiten. Abhängig von Ihrem Geschäftsfall kann es eine gültige Annahme sein, dass all Code möglicherweise eine gewisse Wartung erfordert. In diesem Fall wird die Unterscheidung, die ich hier mit den anderen Antworten mache (z.B. Jason Swetts Antwort ) verschwindet.

Zusammenfassend lässt sich sagen, dass Testbarkeit allein (IMO) kein ausreichender Grund ist, eine Codebasis umzugestalten. Die Testbarkeit spielt eine wichtige Rolle bei der Ermöglichung der Wartung auf Codebasis. Es ist jedoch eine Geschäftsanforderung, die Funktion Ihres Codes zu ändern, die Ihr Refactoring vorantreiben soll. Wenn es keine solchen Geschäftsanforderungen gibt, ist es wahrscheinlich besser, an etwas zu arbeiten, das Ihren Kunden wichtig ist.

(Neuer Code wird natürlich aktiv gepflegt, daher sollte er so geschrieben werden, dass er testbar ist.)

7
Aidan Cully

Ihr Problem hier ist, dass Ihre Testwerkzeuge Mist sind. Sie sollten in der Lage sein, dieses Objekt zu verspotten und Ihre Testmethode aufzurufen, ohne es zu ändern - denn während dieses einfache Beispiel wirklich einfach und leicht zu ändern ist, was passiert, wenn Sie etwas viel Komplizierteres haben.

Viele Leute haben ihren Code geändert, um IoC-, DI- und schnittstellenbasierte Klassen einzuführen, um einfach Unit-Tests mit den Verspottungs- und Unit-Test-Tools zu ermöglichen, die diese Codeänderungen erfordern. Ich denke nicht, dass sie eine gesunde Sache sind, nicht wenn man sieht, dass Code, der ziemlich einfach und unkompliziert war, sich in ein Albtraum-Chaos komplexer Interaktionen verwandelt, das ausschließlich von der Notwendigkeit getrieben wird, jede einzelne Klassenmethode vollständig von allem anderen zu entkoppeln . Und um die Verletzung zusätzlich zu beleidigen, haben wir viele Argumente, ob private Methoden Unit-getestet werden sollten oder nicht! (Natürlich sollten sie, was ist der Sinn von Unit-Tests, wenn Sie nur einen Teil Ihres Systems testen), aber diese Argumente sind eher vom Standpunkt abhängig, dass es schwierig ist, diese Methoden mit den vorhandenen Tools zu testen - stellen Sie sich vor, Ihr Test-Tool könnte dies Führen Sie Tests mit einer privaten Methode genauso einfach durch wie mit einer öffentlichen - jeder würde sie ohne Beanstandung testen.

Das Problem liegt natürlich in der Natur der Testwerkzeuge.

Es gibt jetzt bessere Werkzeuge, die diese Designänderungen für immer ins Bett bringen könnten. Microsoft hat Fakes (nee Moles), mit dem Sie konkrete Objekte, einschließlich statischer, stubben können, sodass Sie Ihren Code nicht mehr ändern müssen, um ihn an das Tool anzupassen. Wenn Sie in Ihrem Fall Fakes verwendet haben, würden Sie den GetTypes-Aufruf durch Ihren eigenen ersetzen, der gültige und ungültige Testdaten zurückgibt. Dies ist ziemlich wichtig, da Ihre vorgeschlagene Änderung dies überhaupt nicht vorsieht.

Um zu antworten: Ihr Kollege hat Recht, aber möglicherweise aus den falschen Gründen. Ändern Sie den zu testenden Code nicht, ändern Sie nicht Ihr Testwerkzeug (oder Ihre gesamte Teststrategie, um mehr Unit-Tests im Integrationsstil anstelle solcher feinkörniger Tests durchzuführen).

Martin Fowler hat eine Diskussion über diesen Bereich in seinem Artikel Mocks sind keine Stubs

2
gbjbaanb

Ich habe im Rahmen von Unit-Tests Tools zur Codeabdeckung verwendet, um zu überprüfen, ob alle Pfade durch den Code ausgeführt werden. Als ziemlich guter Codierer/Tester decke ich normalerweise 80-90% der Codepfade ab.

Wenn ich die nicht abgedeckten Pfade studiere und mich um einige bemühe, entdecke ich Fehler wie Fehlerfälle, die "niemals auftreten". Ja, das Ändern des Codes und das Überprüfen der Testabdeckung führt zu einem besseren Code.

2
Sam Gamoran

Ich denke, Ihr Kollege ist falsch.

Andere haben bereits die Gründe genannt, warum dies eine gute Sache ist, aber solange Sie die Erlaubnis dazu erhalten, sollte es Ihnen gut gehen.

Der Grund für diese Einschränkung ist, dass Änderungen am Code zu Lasten des Codes gehen, der erneut getestet werden muss. Je nachdem, was Sie tun, kann diese Testarbeit für sich genommen eine große Anstrengung sein.

Es ist nicht unbedingt Ihre Aufgabe, die Entscheidung für ein Refactoring zu treffen oder an neuen Funktionen zu arbeiten, die Ihrem Unternehmen/Kunden zugute kommen.

2
ozz

Es gibt einige gravierende Unterschiede zwischen Ihren Beispielen. Im Fall von DoSomethingOnAllTypes() impliziert dies, dass do something Auf Typen in der aktuellen Assembly anwendbar ist. Aber DoSomething(Assembly asm) zeigt explizit an, dass Sie any Assembly an ihn übergeben können.

Der Grund, warum ich darauf hinweise, ist, dass viele Abhängigkeitsinjektionen nur zum Testen außerhalb der Grenzen des ursprünglichen Objekts liegen. Ich weiß, dass Sie gesagt haben " nicht 'alles öffentlich machen' ", aber das ist einer der größten Fehler dieses Modells, dicht gefolgt von diesem: Öffnen des Objektmethoden bis zu Verwendungen, für die sie nicht bestimmt sind.

1
Ross Patterson

Eine gute und gängige Praxis ist die Verwendung von nit-Test- und Debug-Protokolle. Unit-Tests stellen sicher, dass Ihre alten Funktionen nicht beschädigt werden, wenn Sie weitere Programmänderungen vornehmen. Mithilfe von Debug-Protokollen können Sie das Programm zur Laufzeit verfolgen.
Manchmal kommt es vor, dass wir auch darüber hinaus etwas nur zu Testzwecken benötigen. Es ist nicht ungewöhnlich, den Code dafür zu ändern. Es ist jedoch darauf zu achten, dass der Produktionscode dadurch nicht beeinträchtigt wird. In C++ und C wird dies mit dem Makro erreicht, bei dem es sich um eine Entität zur Kompilierungszeit handelt. Dann kommt der Testcode in der Produktionsumgebung überhaupt nicht ins Bild. Ich weiß nicht, ob eine solche Bestimmung in C # vorhanden ist.
Auch wenn Sie Testcode in Ihr Programm einfügen, sollte deutlich sichtbar sein, dass dieser Teil des Codes zu Testzwecken hinzugefügt wird. Oder der Entwickler, der versucht, den Code zu verstehen, wird einfach über diesen Teil des Codes schwitzen.

1
Manoj R

Ihre Frage gab nicht viel Kontext, in dem Ihr Kollege argumentierte, so dass Raum für Spekulationen besteht

"schlechte Praxis" oder nicht hängt davon ab, wie und wann die Änderungen vorgenommen werden.

In meiner Meinung ist Ihr Beispiel zum Extrahieren einer Methode DoSomething(types) in Ordnung.

Aber ich habe Code gesehen, der nicht ok so ist:

public void DoSomethingOnAllTypes()
{
  var types = (!testmode) 
      ? Assembly.GetExecutingAssembly().GetTypes() 
      : getTypesFromTestgenerator();

  foreach (var currentType in types)
  {
     if (!testmode)
     {
        // do something with this type that made the unittest fail and should be skipped.
     }
     // do something with this type (e.g: read it's attributes, process, etc).
  }
}

Diese Änderungen haben das Verständnis des Codes erschwert, da Sie die Anzahl der möglichen Codepfade erhöht haben.

Was ich meine mit wie und wann:

wenn Sie eine funktionierende Implementierung haben und die Änderungen vorgenommen haben, um "Testfunktionen zu implementieren", müssen Sie Ihre Anwendung erneut testen, da Sie möglicherweise Ihre DoSomething() -Methode beschädigt haben.

Die if (!testmode) ist schwieriger zu verstehen und zu testen als die extrahierte Methode.

0
k3b