it-swarm.com.de

Ist das Testverhalten vieler Klassen in einem Test noch ein Unit-Test?

Die Politik unseres Projekts besteht darin, Unit-Tests nur für einzelne Klassen zu schreiben. Alle Abhängigkeiten werden verspottet. Kürzlich haben wir festgestellt, dass dieser Ansatz uns in solchen Fällen verwundbar macht:

Ursprünglich sieht die Klasse A folgendermaßen aus:

class A(val b: B) {

   fun doSomething() {
       b.doSomethingElse()
       b.doSomethingElse2()
   }
}

Und eine solche Klasse A wird mit Unit-Tests abgedeckt. Aufgrund neuer Anforderungen durchläuft die Klasse B das Refactoring und wird hinter einer Schnittstelle versteckt, sodass es technisch möglich ist, dass die Klasse A basierend auf einem Szenario ein anderes Verhalten aufweist.

Das Problem ist, dass wir jetzt, wenn wir den Richtlinien unseres Projekts folgen möchten, auch die Komponententests von A umgestalten sollten. Zuvor gab es einen Test für die ordnungsgemäße Kommunikation mit einem Objekt von B (natürlich verspottet). Wenn nun der Verweis auf B weg ist, wird der Test überarbeitet, um die Kommunikation mit dieser neuen Schnittstelle zu überprüfen.

Ich finde dieses Szenario als Informationsverlust, der während dieses Test-Refactorings auftritt. Wir überprüfen keine Kommunikation mehr zwischen A-> B aufgrund der Reinheit des Komponententests, wenn im realen System eine solche Kommunikation besteht.

Sollten wir vor diesem Hintergrund unsere Denkweise bei Komponententests ändern und Testfälle erstellen, in denen es mehr als ein reales Objekt gibt? Wäre es noch ein Unit-Test oder ist es bereits ein Integrationstest?

5
Mariusz

Die äußere Grenze einer Einheit ist IO. Wenn Sie mit Peripheriegeräten sprechen, werden keine Unit-Tests mehr durchgeführt.

Aber darin können Sie die Dinge so dick oder fein zerschneiden, wie Sie es für richtig halten.

Ein Test kann Methoden aus drei Objekten ausführen und dennoch ein Komponententest sein. Einige mögen behaupten, das sei Integration, aber drei Codezeilen sind genauso "integriert", also ist das nicht wirklich eine Sache. Methoden- und Objektgrenzen stellen keine signifikante Hürde für das Testen dar. Peripheriegeräte tun.

Ein Test ist kein Komponententest, wenn:

  • Es spricht mit der Datenbank
  • Es kommuniziert über das Netzwerk
  • Es berührt das Dateisystem
  • Es kann nicht gleichzeitig mit einem Ihrer anderen Komponententests ausgeführt werden
  • Sie müssen spezielle Dinge an Ihrer Umgebung tun (z. B. das Bearbeiten von Konfigurationsdateien), um sie auszuführen.

Michael Feathers - Effektiv mit Legacy Code arbeiten

Nun können Sie sicher argumentieren, dass einige Leute jeden Test, der durch einen Unit-Test-Rahmen automatisiert werden kann, als Unit-Test betrachten.

Wenn wir nur über Semantik streiten wollen, dann definiere ich eine Einheit als eine schöne Blume in einem Feld, das schlecht riecht.

Unabhängig von den Begriffen ist die Trennung langsamer, mühsamer Tests von Tests erforderlich, die so schnell und stabil sind, dass Sie sie während der Eingabe ausführen können.

Rufen Sie an, was sie tun, was Sie wollen. Verwechsle sie einfach nicht.

12
candied_orange

Alternativer Ansatz zur Kategorisierung von Tests in zwei Kategorien
- Langsame Tests
- Schnelltests

Normalerweise sind langsame Tests Tests, die den Zugriff auf externe Ressourcen erfordern. Auch langsame Tests können Tests sein, die eine sehr, sehr komplizierte Einrichtung erfordern.

Schnelltests sind Tests, mit denen Entwickler während ihrer Arbeit schnelles Feedback erhalten.

Würden Sie jede Klasse explizit testen oder mehrere Klassen innerhalb von Verbrauchertests testen, sollte diese Entscheidung vom Team getroffen werden, wie Arseni Mourzenko bereits in seiner Antwort hervorhob.

Wenn Sie Tests schreiben, bevor Sie Produktionscode schreiben, werden Sie feststellen, dass das Testen mehrerer Klassen den Workflow beschleunigt und wertvolleres Feedback liefert.
Denn wenn Sie anfangen, an einer Funktion zu arbeiten, wissen Sie noch nicht, welche Art von Interaktionen Sie haben würden.
Sie würden also einfach alles in eine Klasse oder eine Methode schreiben, die mit Tests behandelt wird, und danach werden Sie es umgestalten, indem Sie Duplikate oder Abhängigkeiten extrahieren.

2
Fabio

Lassen Sie sich nicht von Definitionen überraschen. Es ist nicht das Wichtige.

Es gibt einige Meinungsverschiedenheiten über die genaue Bedeutung von "Einheit". Der Zweck Ihrer automatisierten Tests besteht jedoch nicht darin, einer willkürlichen Definition zu entsprechen. Der Zweck ist es, Fehler zu erkennen. Oder allgemeiner: Um zu überprüfen, ob das Verhalten des Codes den Erwartungen entspricht.

Beim Refactoring sollten Unit-Tests sicherstellen, dass Sie keine Fehler eingeführt oder das Verhalten geändert haben. Daher sollten Tests nicht an Implementierungsdetails im getesteten Code gekoppelt werden. In der Tat sollte es möglich sein, die Implementierung einer Klasse vollständig neu zu schreiben und die Tests überprüfen zu lassen, ob das Verhalten immer noch dasselbe ist.

Aus dieser Perspektive ist es einem Test egal, ob die getestete Klasse andere Klassen aufruft. Dies ist ein Implementierungsdetail. In der Tat besteht ein typisches Refactoring darin, internen Code in einer Methode in eine separate Klasse zu extrahieren - und genau in diesem Szenario sollten Tests in der Lage sein, zu überprüfen, ob das Refactoring das Verhalten nicht geändert hat.

Wenn Sie jedoch entscheiden, dass ein Test nur eine einzelne Klasse testen soll, koppeln Sie den Test mit Implementierungsdetails. Sie können die Implementierung nicht sicher umgestalten, da Sie möglicherweise auch den Test ändern und Mocks einführen müssen. Das Ändern des Tests zur gleichen Zeit wie das Ändern des getesteten Codes macht jedoch den gesamten Testzweck zunichte, da Sie nicht sicher sein können, ob Sie dasselbe wie zuvor überprüfen.

Besonders heimtückisch ist die Art der Verspottung, bei der Sie nur überprüfen, ob bestimmte Methoden mit bestimmten Parametern für die Verspottung aufgerufen werden. Diese Form von Testpaaren ist so eng mit der Implementierung verbunden, dass sie eher ein Hindernis für das Refactoring als eine Hilfe darstellen.

Fazit: Vermeiden Sie Verspottungen (außer bei externen Diensten oder nicht deterministischen Eingaben) und lassen Sie die Tests so viele Klassen ausüben, wie zur Überprüfung des zu testenden Verhaltens erforderlich sind.

Persönlich stelle ich mir "Einheit" gerne als "Verhaltenseinheit" vor - den kleinsten Teil einer Spezifikation, der unabhängig getestet werden kann. Aber das ist nur meine Definition.

2
JacquesB

In Ihrer überarbeiteten Klasse A sagen Sie, dass sie nicht mehr für die direkte Kommunikation mit der Klasse B verantwortlich ist, sondern für die Kommunikation mit einer Interface. Solange Sie diese Kommunikation testen, sollten Sie sich keine Sorgen mehr machen müssen, dass Sie nicht mehr direkt mit der spezifischen Klasse B kommunizieren.

Die Klasse A kennt die Klasse B nicht, daher sollten auch die Tests der Klasse A nicht ausgeführt werden.

Um das Verhalten der Klasse B zu testen (falls die Klasse A oder eine andere Klasse zur Laufzeit eine Instanz der Klasse B aufruft), sollten Sie andere Tests für die Klasse durchführen B.

1
Eric King

Ist das Testverhalten vieler Klassen in einem Test noch ein Unit-Test?

Dies hängt davon ab, wessen Definition des Einheitentests als maßgeblich angesehen wird.

sollten wir unsere Denkweise bei Komponententests ändern und Testfälle erstellen, in denen es mehr als ein reales Objekt gibt? Wäre es noch ein Unit-Test oder ist es bereits ein Integrationstest?

Wenn Sie rückwärts inkompatible Änderungen vornehmen, brechen Dinge, die davon abhängen, was Sie geändert haben. Wenn Ihre Tests Sie jetzt anschreien, ist das großartig - alles funktioniert so, wie es soll.

Zu diesem Zeitpunkt haben Sie mehrere Möglichkeiten. Eine besteht darin, die Änderungen, die Sie an Ihrem Design vornehmen, so zu überdenken, dass sie abwärtskompatibel sind. Dies bedeutet normalerweise, neue Klassen und Schnittstellen zu erstellen und die alten Anwendungsfälle in Bezug auf die neuen Elemente zu implementieren. Tatsächlich wird der alte Code überarbeitet, um gegebenenfalls die neue Vorgehensweise zu nutzen, und die Tests bestätigen, dass Sie dabei keine neuen Fehler eingeführt haben. Ihre neuen Komponententests decken die neuen Verhaltensweisen ab.

Randnotiz: In diesem Schritt können Sie die alte Vorgehensweise als Verwaltungsmethode für die Änderung ablehnen. Auf diese Weise können Sie das Hinzufügen des Neuen vom Entfernen des Alten zeitlich trennen.

Eine andere Möglichkeit besteht darin, dass Sie entscheiden, dass diese Änderung notwendig ist, und dies ist der richtige Zeitpunkt, um die Kosten dafür zu bezahlen. Entfernen Sie einfach den Problemtest. Ta-da! und du bist fertig. Perfekte Wahl, wenn Sie sowohl das Baby als auch das Badewasser ersetzen.

Die Problemfälle liegen in der Mitte; Sie begehen eine "kleine" inkompatible Änderung, und der größte Teil des Wertes der Tests ist noch vorhanden, aber für diesen Wert ist weiterhin ein unverhältnismäßiger Betrag erforderlich der Arbeit.

Oft bedeutet dies, dass Ihre Tests viele volatile Entscheidungen umfassen (siehe Parnas, 1971 ) und dass Ihr aktuelles Design das Austauschen von Elementen zu teuer macht.

Zum Beispiel ist es einfach vorzutäuschen, dass ein Verhalten eine einzelne atomare Sache ist, während es in der Praxis tatsächlich eine Ansammlung einer Reihe verschiedener unabhängiger Ideen ist. Wenn Sie Ihre Tests so aufteilen, dass sie sich besser auf die Verhaltensideen konzentrieren (anstatt sich über strukturelle Probleme wie "Klasse" Gedanken zu machen), dann testen Sie Tests, die außerhalb ihrer unmittelbaren Bedenken widerstandsfähig sind, Änderungen vorzunehmen.

Aber seien wir ehrlich, um in diesen Zustand zu gelangen, muss man sich Gedanken und Design machen.

Vielleicht möchten Sie James Shores Testen ohne Mocks überprüfen; oder einige der veröffentlichten Artikel zu Property Based Testing , in denen häufig ein einzelnes Verhalten in mehrere verschiedene Tests unterteilt wird, sodass Sie viele Ihrer Tests auch dann beibehalten/wiederverwenden können, wenn die Gesamtverhalten ändert sich.

1
VoiceOfUnreason

Historisch gesehen testet ein Komponententest das Verhalten einer einzelnen Komponente, einer Klasse. Um sofort Regressionsfehler zu finden, bei denen die Klasse das erwartete Verhalten nicht mehr erfüllt.

Unit-Tests werden heutzutage jedoch für viel, viel mehr verwendet.

  • Testgetriebene Entwicklung

    Dies ist ein schneller Weg, um Teile von Anfang an isoliert/fusselig mit einem gesicherten Verhalten zu entwickeln.

  • Datenintegrität

    Ich habe Überprüfungen von XMLs von Geschäftsprozessabläufen gesehen, um zu überprüfen, ob es immer einen bedingungslosen Standardfluss zu einem nächsten Status gibt. Diese Tests garantieren, dass die harten Datenressourcen keine möglichen Fehler enthalten. Ihr eigener prüfender "Compiler" sozusagen. Das gleiche kann gelten, um die Übersetzungen mehrerer Sprachen zu vergleichen. (Für Ressourcendateien habe ich das Testen von all Dateien in einem Ressourcenverzeichnisbaum in einem einzigen Komponententest mit einem Klassenpfad-Verzeichnislauf gesehen, sodass zukünftige Ressourcen automatisch getestet werden.)

Bereits diese Fälle zeigen, dass Unit Testing tatsächlich als Qualitätssicherung Maß verwendet wird.

Daher können Sie auch Komponentencontainer und das gesamte Systemverhalten testen. Die Idee, nur Einheiten zu testen (nach dem Schreiben der Einheiten) ist veraltet.

1
Joop Eggen