Führen Unit-Tests zu einer vorzeitigen Verallgemeinerung (speziell im Kontext von C ++)?


20

Vorbemerkungen

Ich werde nicht auf die Unterscheidung der verschiedenen Testarten eingehen, da gibt es auf diesen Seiten bereits einige Fragen dazu .

Ich nehme, was da ist und was sagt: Komponententest im Sinne von "Testen der kleinsten isolierbaren Einheit einer Anwendung", aus der diese Frage tatsächlich stammt

Das Isolationsproblem

Was ist die kleinste isolierbare Einheit eines Programms. Nun, wie ich es sehe, hängt es (hoch?) Davon ab, in welcher Sprache Sie programmieren.

Micheal Feathers spricht über das Konzept einer Naht : [WEwLC, S. 31]

Eine Naht ist ein Ort, an dem Sie das Verhalten in Ihrem Programm ändern können, ohne es dort zu bearbeiten.

Und ohne auf die Details einzugehen, verstehe ich eine Naht - im Rahmen von Unit-Tests - als einen Ort in einem Programm, an dem Ihr "Test" mit Ihrem "Gerät" kommunizieren kann.

Beispiele

Bei Unit-Tests - insbesondere in C ++ - müssen aus dem getesteten Code weitere Nähte hinzugefügt werden , die für ein bestimmtes Problem unbedingt erforderlich wären.

Beispiel:

  • Hinzufügen einer virtuellen Schnittstelle, bei der eine nicht virtuelle Implementierung ausreichend gewesen wäre
  • Teilen - Verallgemeinern (?) - eine (kleinere) Klasse, die "nur" das Hinzufügen eines Tests erleichtert.
  • Teilen Sie ein einzeln ausführbares Projekt in scheinbar "unabhängige" Bibliotheken auf, "nur" um sie für die Tests unabhängig kompilieren zu können.

Die Frage

Ich werde ein paar Versionen ausprobieren, die hoffentlich nach dem gleichen Punkt fragen:

  • Ist die Art und Weise, wie Unit-Tests erfordern, dass der Code einer Anwendung "nur" für die Unit-Tests strukturiert wird, oder ist sie tatsächlich für die Anwendungsstruktur von Vorteil?
  • Ist die Codeverallgemeinerung, die erforderlich ist, um die Einheit testbar zu machen, für etwas anderes als die Einheitentests nützlich ?
  • Erzwingt das Hinzufügen von Komponententests eine unnötige Verallgemeinerung?
  • Ist die Formeinheitentestkraft auf Code "immer" auch eine gute Form für den Code im Allgemeinen, wie aus der Problemdomäne ersichtlich?

Ich erinnere mich an eine Faustregel, die besagte, verallgemeinern Sie nicht, bis Sie / müssen, bis es einen zweiten Platz gibt, der den Code verwendet. Bei Unit-Tests gibt es immer einen zweiten Ort, an dem der Code verwendet wird, nämlich den Unit-Test. Also ist dieser Grund genug, um zu verallgemeinern?


8
Ein weit verbreitetes Mem ist, dass jedes Muster überbeansprucht werden kann, um ein Anti-Muster zu werden. Gleiches gilt für TDD. Man kann testbare Schnittstellen hinzufügen, die über den Punkt hinausgehen, an dem die Rendite abnimmt, wobei der getestete Code kleiner ist als die hinzugefügten allgemeinen Testschnittstellen, sowie in den Bereich mit zu geringem Kosten-Nutzen. Ein Casual Game mit zusätzlichen Schnittstellen zum Testen wie ein Deep Space Mission OS könnte sein Marktfenster komplett verfehlen. Stellen Sie sicher, dass der hinzugefügte Test vor diesen Wendepunkten liegt.
hotpaw2

@ hotpaw2 Blasphemie! :)
maple_shaft

Antworten:


23

Bei Unit-Tests - insbesondere in C ++ - müssen aus dem getesteten Code weitere Nähte hinzugefügt werden, die für ein bestimmtes Problem unbedingt erforderlich wären.

Nur wenn Sie das Testen nicht als wesentlichen Bestandteil der Problemlösung betrachten. Für jedes nicht triviale Problem sollte dies nicht nur in der Software-Welt der Fall sein.

In der Hardware-Welt hat man das längst gelernt - auf die harte Tour. Die Hersteller verschiedener Geräte haben über Jahrhunderte hinweg durch unzählige fallende Brücken, explodierende Autos, rauchende CPUs usw. usw. gelernt, was wir jetzt in der Software-Welt lernen. Alle von ihnen bauen "zusätzliche Nähte" in ihre Produkte ein, um sie testbar zu machen. Heutzutage verfügen die meisten Neuwagen über Diagnoseanschlüsse, über die die Monteure Daten über den Motorinneren abrufen können. Ein erheblicher Teil der Transistoren in jeder CPU dient Diagnosezwecken. In der Hardware-Welt kostet jedes bisschen "extra" Material, und wenn ein Produkt millionenfach hergestellt wird, summieren sich diese Kosten mit Sicherheit zu großen Geldsummen. Dennoch sind die Hersteller bereit, all dieses Geld für Testbarkeit auszugeben.

Zurück zur Software-Welt ist C ++ in der Tat schwieriger zu testen als spätere Sprachen mit dynamischem Klassenladen, Reflexion usw. Dennoch können die meisten Probleme zumindest gemildert werden. In dem einen C ++ - Projekt, in dem ich bisher Unit-Tests verwendet habe, haben wir die Tests nicht so oft ausgeführt wie in einem Java-Projekt - aber sie waren immer noch Teil unseres CI-Builds, und wir fanden sie nützlich.

Ist die Art und Weise, wie Unit-Tests erfordern, dass der Code einer Anwendung "nur" für die Unit-Tests strukturiert wird, oder ist sie tatsächlich für die Anwendungsstruktur von Vorteil?

Nach meiner Erfahrung ist ein prüfbares Design insgesamt nicht "nur" für die Gerätetests selbst von Vorteil. Diese Vorteile kommen auf verschiedenen Ebenen:

  • Wenn Sie Ihr Design testbar machen, müssen Sie Ihre Anwendung in kleine, mehr oder weniger unabhängige Teile aufteilen, die sich nur in begrenzter und klar definierter Weise gegenseitig beeinflussen können - dies ist für die langfristige Stabilität und Wartbarkeit Ihres Programms sehr wichtig. Ohne dies verschlechtert sich der Code tendenziell in Spaghetti-Code, bei dem jede Änderung an einem Teil der Codebasis unerwartete Auswirkungen auf scheinbar nicht zusammenhängende, unterschiedliche Teile des Programms haben kann. Das ist natürlich der Albtraum eines jeden Programmierers.
  • Wenn Sie die Tests selbst in TDD-Form schreiben, werden Ihre APIs, Klassen und Methoden tatsächlich getestet. Dies ist ein sehr effektiver Test, um festzustellen, ob Ihr Design sinnvoll ist. Wenn Sie Tests gegen und Schnittstellen schreiben, die sich umständlich oder schwierig anfühlen, erhalten Sie wertvolles frühes Feedback, wenn dies der Fall ist immer noch einfach, die API zu gestalten. Mit anderen Worten, dies schützt Sie davor, Ihre APIs vorzeitig zu veröffentlichen.
  • Das von TDD erzwungene Entwicklungsmuster hilft Ihnen, sich auf die konkrete (n) Aufgabe (n) zu konzentrieren, und hält Sie auf dem Laufenden, wodurch die Wahrscheinlichkeit minimiert wird, dass Sie andere Probleme als die von Ihnen erwarteten lösen und unnötige zusätzliche Funktionen und Komplexität hinzufügen , etc.
  • Die schnelle Rückmeldung von Komponententests ermöglicht es Ihnen, den Code kühn umzugestalten. So können Sie das Design während der gesamten Lebensdauer des Codes ständig anpassen und weiterentwickeln und so die Code-Entropie wirksam verhindern.

Ich erinnere mich an eine Faustregel, die besagte, verallgemeinern Sie nicht, bis Sie / müssen, bis es einen zweiten Platz gibt, der den Code verwendet. Bei Unit-Tests gibt es immer einen zweiten Ort, an dem der Code verwendet wird, nämlich den Unit-Test. Reicht dieser Grund also aus, um zu verallgemeinern?

Wenn Sie nachweisen können, dass Ihre Software genau das tut, was sie tun soll - und dies schnell, wiederholbar, billig und deterministisch genug, um Ihre Kunden zufriedenzustellen -, ohne die durch Unit-Tests erzwungene "zusätzliche" Verallgemeinerung oder Nahtstellen (und lass uns wissen, wie du es machst, da ich mir sicher bin, dass viele Leute in diesem Forum genauso interessiert wären wie ich :-)

Übrigens meine ich mit "Verallgemeinerung" Dinge wie die Einführung einer Schnittstelle (abstrakte Klasse) und Polymorphismus anstelle einer einzelnen konkreten Klasse - wenn nicht, bitte klarstellen.


Sir, ich grüße Sie.
GordonM

Ein kurzer, aber umständlicher Hinweis: Der "Diagnose-Port" ist meistens dort, weil die Regierungen sie als Teil eines Emissionskontrollsystems vorgeschrieben haben. Infolgedessen weist es schwerwiegende Einschränkungen auf; Es gibt viele Dinge, die möglicherweise mit diesem Anschluss diagnostiziert werden könnten, die nicht vorliegen (dh alles, was nicht mit der Emissionskontrolle zu tun hat).
Robert Harvey

4

Ich werde den Weg des Testivus nach dir werfen , aber um zusammenzufassen:

Wenn Sie viel Zeit und Energie darauf verwenden, Ihren Code zu komplizieren, um einen einzelnen Teil des Systems zu testen, kann es sein, dass Ihre Struktur falsch ist oder dass Ihr Testansatz falsch ist.

Die einfachste Anleitung ist folgende: Sie testen die öffentliche Schnittstelle Ihres Codes in der Weise, wie sie von anderen Teilen des Systems verwendet werden soll.

Wenn Ihre Tests lang und kompliziert werden, ist dies ein Hinweis darauf, dass die Verwendung der öffentlichen Schnittstelle schwierig sein wird.

Wenn Sie die Vererbung verwenden müssen, um zu ermöglichen, dass Ihre Klasse von einer anderen als der einzelnen Instanz verwendet wird, für die sie derzeit verwendet wird, besteht eine gute Chance, dass Ihre Klasse zu stark in ihre Verwendungsumgebung eingebunden ist. Können Sie ein Beispiel für eine Situation geben, in der dies zutrifft?

Hüten Sie sich jedoch vor dem Dogma der Einheitentests. Schreiben Sie den Test, der es Ihnen ermöglicht, das Problem zu erkennen, das den Client dazu veranlasst, Sie anzuschreien .


Ich wollte dasselbe hinzufügen: mache ein API, teste das API von außen.
Christopher Mahan

2

TDD und Unit Testing sind gut für das gesamte Programm und nicht nur für die Unit Tests. Der Grund dafür ist, dass es gut für das Gehirn ist.

Dies ist eine Präsentation über ein bestimmtes ActionScript-Framework namens RobotLegs. Wenn Sie jedoch die ersten 10 Folien durchblättern, kommen Sie zu den guten Teilen des Gehirns.

Durch TDD- und Unit-Tests werden Sie gezwungen, sich so zu verhalten, dass das Gehirn Informationen besser verarbeiten und speichern kann. Während also Ihre genaue Aufgabe vor Ihnen darin besteht, einen besseren Komponententest durchzuführen oder den Code für die Komponententests besser zu machen, wird Ihr Code tatsächlich besser lesbar und somit besser wartbar. Auf diese Weise können Sie schneller in Gewohnheiten programmieren und Ihren Code schneller nachvollziehen, wenn Sie Funktionen hinzufügen / entfernen, Fehler beheben oder im Allgemeinen die Quelldatei öffnen müssen.


1

Testen der kleinsten isolierbaren Einheit einer Anwendung

Das ist wahr, aber wenn Sie zu weit gehen, gibt es Ihnen nicht viel, und es kostet viel, und ich glaube, dass es dieser Aspekt ist, der die Verwendung des Begriffs BDD als das fördert, was TDD alles hätte sein sollen mitmachen - die kleinste isolierbare Einheit ist das, was Sie wollen.

Ich habe zum Beispiel einmal eine Netzwerkklasse debuggt, die (unter anderem) 2 Methoden hatte: 1 zum Festlegen der IP-Adresse, eine andere zum Festlegen der Portnummer. Natürlich waren dies sehr einfache Methoden, die den einfachsten Test problemlos bestehen würden. Wenn Sie jedoch die Portnummer und anschließend die IP-Adresse festlegen, funktioniert dies nicht - der IP-Setter überschreibt die Portnummer mit einem Standardwert. Sie mussten also die Klasse als Ganzes testen, um ein korrektes Verhalten sicherzustellen. Ich denke, dass das Konzept von TDD fehlt, aber BDD gibt es Ihnen. Sie müssen nicht wirklich jede winzige Methode testen, um den sinnvollsten und kleinsten Bereich der gesamten Anwendung zu testen - in diesem Fall die Netzwerkklasse.

Letztendlich gibt es keine magische Kugel zum Testen, Sie müssen vernünftige Entscheidungen darüber treffen, wie viel und mit welcher Granularität Ihre begrenzten Testressourcen angewendet werden sollen. Der werkzeugbasierte Ansatz, der automatisch Stubs generiert, tut dies nicht, sondern ist ein stumpfer Ansatz.

Vor diesem Hintergrund müssen Sie Ihren Code nicht in einer bestimmten Weise strukturieren, um TDD zu erreichen. Die Teststufe hängt jedoch von der Struktur Ihres Codes ab - wenn Sie eine monolithische GUI haben, an die die gesamte Logik fest gebunden ist Wenn Sie die GUI-Struktur verwenden, wird es schwieriger, diese Teile zu isolieren. Sie können jedoch einen Komponententest schreiben, bei dem sich 'unit' auf die GUI bezieht und die gesamte Back-End-DB-Arbeit verspottet wird. Dies ist ein extremes Beispiel, aber es zeigt, dass Sie weiterhin automatisierte Tests durchführen können.

Ein Nebeneffekt der Strukturierung Ihres Codes, um das Testen kleinerer Einheiten zu vereinfachen, hilft Ihnen dabei, die Anwendung besser zu definieren, und ermöglicht es Ihnen, Teile einfacher auszutauschen. Dies hilft auch beim Codieren, da es weniger wahrscheinlich ist, dass 2 Entwickler zu einem bestimmten Zeitpunkt an derselben Komponente arbeiten - im Gegensatz zu einer monolithischen App, bei der Abhängigkeiten miteinander vermischt sind, die die Arbeit aller anderen Benutzer unterbrechen, kommt die Verschmelzungszeit.


0

Sie haben eine gute Erkenntnis über Kompromisse bei der Sprachgestaltung erzielt. Einige der wichtigsten Entwurfsentscheidungen in C ++ (der virtuelle Funktionsmechanismus gemischt mit dem statischen Funktionsaufrufmechanismus) machen TDD schwierig. Die Sprache unterstützt nicht wirklich das, was Sie brauchen, um es einfach zu machen. Es ist einfach, C ++ zu schreiben, das für einen Komponententest so gut wie unmöglich ist.

Wir hatten besseres Glück, unseren TDD C ++ - Code aus einer quasi-funktionalen Denkweise heraus zu schreiben - Funktionen, nicht Prozeduren (eine Funktion, die keine Argumente akzeptiert und nichtig ist) und wo immer möglich Kompositionen zu verwenden. Da es schwierig ist, diese Mitgliederklassen zu ersetzen, konzentrieren wir uns darauf, diese Klassen zu testen, um eine vertrauenswürdige Basis aufzubauen, und dann zu wissen, dass die Basiseinheit funktioniert, wenn wir sie zu etwas anderem hinzufügen.

Der Schlüssel ist der quasi-funktionale Ansatz. Denken Sie darüber nach, wenn Ihr gesamter C ++ - Code aus freien Funktionen besteht, die auf keine globalen Elemente zugreifen, wäre dies ein Kinderspiel für den Komponententest :)

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.