Einige Leute behaupten, dass Integrationstests alle Arten von schlecht und falsch sind - alles muss Unit-getestet sein, was bedeutet, dass man Abhängigkeiten verspotten muss; Eine Option, die ich aus verschiedenen Gründen nicht immer mag.
Ich finde, dass ein Unit-Test in einigen Fällen einfach nichts beweist.
Nehmen wir als Beispiel die folgende (triviale, naive) Repository-Implementierung (in PHP):
class ProductRepository
{
private $db;
public function __construct(ConnectionInterface $db) {
$this->db = $db;
}
public function findByKeyword($keyword) {
// this might have a query builder, keyword processing, etc. - this is
// a totally naive example just to illustrate the DB dependency, mkay?
return $this->db->fetch("SELECT * FROM products p"
. " WHERE p.name LIKE :keyword", ['keyword' => $keyword]);
}
}
Angenommen, ich möchte in einem Test nachweisen, dass dieses Repository tatsächlich Produkte finden kann, die mit verschiedenen angegebenen Schlüsselwörtern übereinstimmen.
Wie kann ich nach einem Integrationstest mit einem realen Verbindungsobjekt feststellen, dass tatsächlich echte Abfragen generiert werden - und dass diese Abfragen tatsächlich das tun, was ich denke, dass sie tun?
Wenn ich das Verbindungsobjekt in einem Unit-Test verspotten muss, kann ich nur beweisen, dass "es die erwartete Abfrage generiert" - aber das bedeutet nicht, dass es tatsächlich funktionieren wird ... das heißt, dass es möglicherweise die Abfrage generiert Ich habe es erwartet, aber vielleicht macht diese Abfrage nicht das, was ich denke.
Mit anderen Worten, ich fühle mich wie ein Test, der Aussagen über die generierte Abfrage macht, im Wesentlichen wertlos ist, weil er testet, wie die findByKeyword()
Methode implementiert wurde , aber das beweist nicht, dass sie tatsächlich funktioniert .
Dieses Problem ist nicht auf Repositorys oder die Datenbankintegration beschränkt - es scheint in vielen Fällen zutreffend zu sein, in denen Aussagen über die Verwendung eines Mocks (Test-Double) nur beweisen, wie die Dinge implementiert sind, nicht, ob sie implementiert werden eigentlich arbeiten.
Wie gehen Sie mit solchen Situationen um?
Sind Integrationstests in einem solchen Fall wirklich "schlecht"?
Ich verstehe, dass es besser ist, eine Sache zu testen, und ich verstehe auch, warum Integrationstests zu unzähligen Codepfaden führen, die alle nicht getestet werden können - aber im Fall eines Dienstes (wie z. B. eines Repositorys), dessen einziger Zweck darin besteht Wie können Sie ohne Integrationstests wirklich etwas testen, um mit einer anderen Komponente zu interagieren?
quelle
Antworten:
Ihr Kollege hat Recht, dass alles, was Unit-Tests unterzogen werden können, Unit-Tests unterzogen werden sollten, und Sie haben Recht, dass Unit-Tests Sie nur so weit und nicht weiter bringen, insbesondere wenn Sie einfache Wrapper für komplexe externe Services schreiben.
Eine übliche Denkweise beim Testen ist eine Testpyramide . Es ist ein Konzept, das häufig mit Agile in Verbindung steht, und viele haben darüber geschrieben, darunter Martin Fowler (der es Mike Cohn beim Erfolg von Agile zuschreibt ), Alistair Scott und der Google Testing Blog .
Der Gedanke ist, dass schnell ablaufende, ausfallsichere Komponententests die Grundlage des Testprozesses bilden. Es sollten konzentriertere Komponententests als System- / Integrationstests und mehr System- / Integrationstests als End-to-End-Tests vorhanden sein. Wenn Sie sich der Spitze nähern, benötigen die Tests in der Regel mehr Zeit und Ressourcen, sind spröder und instabiler und können weniger genau feststellen, welches System oder welche Datei defekt ist . Natürlich ist es vorzuziehen, nicht "kopflastig" zu sein.
Bis zu diesem Punkt sind Integrationstests nicht schlecht , aber starkes Vertrauen in sie kann darauf hinweisen, dass Sie Ihre einzelnen Komponenten nicht so entworfen haben, dass sie einfach zu testen sind. Denken Sie daran, dass das Ziel hier darin besteht, zu testen, ob Ihr Gerät seinen Spezifikationen entspricht, und dabei ein Minimum an anderen zerbrechlichen Systemen zu berücksichtigen : Möglicherweise möchten Sie eine speicherinterne Datenbank testen (die ich neben Mocks als ein für Unit-Tests geeignetes Test-Double bezeichne ) zum Beispiel für umfangreiche Edge-Case-Tests, und schreiben Sie dann einige Integrationstests mit der realen Datenbank-Engine, um festzustellen, ob die Hauptfälle beim Zusammenbau des Systems funktionieren.
Als Randnotiz haben Sie erwähnt, dass die Mocks, die Sie schreiben, lediglich testen, wie etwas implementiert ist, nicht, ob es funktioniert . Das ist so etwas wie ein Gegenmuster: Ein Test, der ein perfekter Spiegel seiner Implementierung ist, testet überhaupt nichts. Testen Sie stattdessen, ob sich jede Klasse oder Methode gemäß ihrer eigenen Spezifikation verhält , unabhängig von der erforderlichen Abstraktion oder dem erforderlichen Realismus.
quelle
Das ist ein bisschen wie zu sagen, dass Antibiotika schlecht sind - alles sollte mit Vitaminen geheilt werden.
Unit-Tests können nicht alles erfassen - sie testen nur, wie eine Komponente in einer kontrollierten Umgebung funktioniert . Integrationstests bestätigen, dass alles zusammenarbeitet , was schwieriger, aber letztendlich aussagekräftiger ist.
Ein guter, umfassender Testprozess verwendet beide Arten von Tests - Komponententests zur Überprüfung von Geschäftsregeln und anderen Dingen, die unabhängig voneinander getestet werden können, und Integrationstests, um sicherzustellen, dass alles zusammenarbeitet.
Sie können es auf Datenbankebene testen . Führen Sie die Abfrage mit verschiedenen Parametern aus und prüfen Sie, ob Sie die erwarteten Ergebnisse erhalten. Zugegeben, es bedeutet, dass alle Änderungen zurück in den "wahren" Code kopiert / eingefügt werden. aber es wird Ihnen erlauben , die Abfrage unabhängig von irgendwelchen anderen Abhängigkeiten zu testen.
quelle
Unit Tests erkennen nicht alle Mängel. Aber sie sind billiger einzurichten und (erneut) auszuführen als andere Arten von Tests. Die Unit-Tests sind durch die Kombination von moderatem Wert und niedrigen bis moderaten Kosten gerechtfertigt.
In der folgenden Tabelle sind die Fehlererkennungsraten für verschiedene Testarten aufgeführt.
Quelle: S.470 in Code Complete 2 von McConnell
quelle
Nein, sie sind nicht schlecht. Hoffentlich sollte man Unit- und Integrationstests haben. Sie werden in verschiedenen Phasen des Entwicklungszyklus verwendet und ausgeführt.
Unit-Tests
Unit-Tests sollten auf dem Build-Server und lokal ausgeführt werden, nachdem der Code kompiliert wurde. Wenn ein Komponententest fehlschlägt, sollte der Build fehlschlagen oder die Code-Aktualisierung erst durchgeführt werden, wenn die Tests behoben sind. Der Grund, warum wir Komponententests isoliert haben möchten, ist, dass der Build-Server alle Tests ohne alle Abhängigkeiten ausführen kann. Dann könnten wir den Build ohne all die komplexen Abhängigkeiten ausführen und viele Tests durchführen, die sehr schnell ablaufen.
Für eine Datenbank sollte man also ungefähr Folgendes haben:
Jetzt geht die eigentliche Implementierung von IRepository in die Datenbank, um die Produkte abzurufen, aber für Komponententests kann man IRepository mit einer Fälschung verspotten, um alle Tests nach Bedarf ohne eine Actaul-Datenbank auszuführen, da wir alle Arten von Produktlisten simulieren können von der Scheininstanz zurückgegeben werden und eine Geschäftslogik mit den gespielten Daten testen.
Integrationstests
Integrationstests sind typischerweise Grenzüberschreitungstests. Wir möchten diese Tests auf dem Bereitstellungsserver (der realen Umgebung), in der Sandbox oder sogar lokal (auf die Sandbox verwiesen) ausführen. Sie werden nicht auf dem Buildserver ausgeführt. Nachdem die Software in der Umgebung bereitgestellt wurde, werden diese normalerweise als Aktivitäten nach der Bereitstellung ausgeführt. Sie können über Befehlszeilendienstprogramme automatisiert werden. Zum Beispiel können wir nUnit über die Befehlszeile ausführen, wenn wir alle Integrationstests, die wir aufrufen möchten, kategorisieren. Diese rufen tatsächlich das reale Repository mit dem realen Datenbankaufruf auf. Diese Art von Tests helfen bei:
Diese Tests sind manchmal schwieriger durchzuführen, da wir sie möglicherweise auch einrichten und / oder abbauen müssen. Erwägen Sie das Hinzufügen eines Produkts. Wahrscheinlich möchten wir das Produkt hinzufügen, es abfragen, um festzustellen, ob es hinzugefügt wurde, und es anschließend entfernen, nachdem wir fertig sind. Wir möchten keine 100er oder 1000er "Integrations" -Produkte hinzufügen, daher ist eine zusätzliche Einrichtung erforderlich.
Die Integrationstests können sich als sehr wertvoll erweisen, um eine Umgebung zu validieren und sicherzustellen, dass die Realität funktioniert.
Man sollte beides haben.
quelle
Datenbankintegrationstests sind nicht schlecht. Mehr noch, sie sind notwendig.
Sie haben Ihre Anwendung wahrscheinlich in Ebenen aufgeteilt, und es ist eine gute Sache. Sie können jede Ebene einzeln testen, indem Sie benachbarte Ebenen verspotten, und das ist auch gut so. Ganz gleich, wie viele Abstraktionsebenen Sie erstellen, irgendwann muss es eine Ebene geben, die die Dirty-Arbeit erledigt - tatsächlich mit der Datenbank sprechen. Wenn Sie es nicht testen, testen Sie überhaupt nicht . Wenn Sie die Ebene n testen, indem Sie die Ebene n-1 verspotten, bewerten Sie die Annahme, dass die Ebene n unter der Bedingung funktioniert , dass die Ebene n-1 funktioniert. Damit dies funktioniert, müssen Sie irgendwie beweisen, dass Schicht 0 funktioniert.
Während Sie theoretisch eine Unit-Test-Datenbank erstellen könnten, indem Sie generiertes SQL analysieren und interpretieren, ist es viel einfacher und zuverlässiger, eine Test-Datenbank im laufenden Betrieb zu erstellen und mit ihr zu kommunizieren.
Fazit
Welches Vertrauen wird durch Unit-Tests Ihres Abstract Repository , Ethereal Object-Relational-Mapper , generischen aktiven Datensatzes und theoretischen Persistenz- Layers gewonnen, wenn Ihr generiertes SQL letztendlich einen Syntaxfehler enthält?
quelle
Du brauchst beides.
Wenn Sie in Ihrem Beispiel getestet haben, dass sich eine Datenbank in einem bestimmten Zustand befindet, erhalten Sie beim
findByKeyword
Ausführen der Methode die Daten zurück, von denen Sie erwarten, dass dies ein guter Integrationstest ist.In jedem anderen Code, der diese
findByKeyword
Methode verwendet, möchten Sie steuern, was in den Test eingespeist wird, damit Sie Nullen oder die richtigen Wörter für Ihren Test oder was auch immer zurückgeben können. Dann verspotten Sie die Datenbankabhängigkeit, damit Sie genau wissen, was Ihr Test bewirken wird empfangen (und Sie verlieren den Overhead, wenn Sie eine Verbindung zu einer Datenbank herstellen und sicherstellen, dass die darin enthaltenen Daten korrekt sind)quelle
Der Autor des Blog-Artikels, auf den Sie verweisen, befasst sich hauptsächlich mit der potenziellen Komplexität, die sich aus integrierten Tests ergeben kann (obwohl er sehr eigenwillig und kategorisch geschrieben ist). Integrierte Tests sind jedoch nicht unbedingt schlecht, und einige sind sogar nützlicher als reine Komponententests. Es hängt wirklich vom Kontext Ihrer Anwendung und dem ab, was Sie testen möchten.
Viele Anwendungen funktionieren heutzutage einfach überhaupt nicht, wenn der Datenbankserver ausfällt. Denken Sie zumindest an die Funktion, die Sie testen möchten.
Auf der einen Seite, wenn das, was Sie testen möchten, nicht von der Datenbank abhängt oder überhaupt nicht davon abhängig gemacht werden kann, schreiben Sie Ihren Test so, dass er nicht einmal versucht, den zu verwenden Datenbank (geben Sie nur nach Bedarf Scheindaten an). Wenn Sie beispielsweise versuchen, eine Authentifizierungslogik beim Bereitstellen einer Webseite zu testen, ist es wahrscheinlich eine gute Sache, diese vollständig von der Datenbank zu trennen (vorausgesetzt, Sie verlassen sich bei der Authentifizierung nicht auf die Datenbank oder auf diese Sie können es einigermaßen leicht verspotten).
Wenn es sich andererseits um eine Funktion handelt, die sich direkt auf Ihre Datenbank stützt und in einer realen Umgebung überhaupt nicht funktioniert, sollte die Datenbank nicht verfügbar sein, und Sie sollten sich darüber lustig machen, was die Datenbank in Ihrem DB-Client-Code tut (dh auf der Ebene, die diese verwendet) DB) macht nicht unbedingt Sinn.
Wenn Sie beispielsweise wissen, dass Ihre Anwendung auf eine Datenbank (und möglicherweise auf ein bestimmtes Datenbanksystem) angewiesen ist, ist es oft Zeitverschwendung, das Datenbankverhalten zu verspotten. Datenbank-Engines (insbesondere RDBMS) sind komplexe Systeme. Einige SQL-Zeilen können tatsächlich viel Arbeit verrichten, was schwierig zu simulieren ist (wenn Ihre SQL-Abfrage einige Zeilen lang ist, benötigen Sie wahrscheinlich viel mehr Java / PHP / C # / Python-Zeilen) Code, um intern dasselbe Ergebnis zu erzielen): Das Duplizieren der bereits in der Datenbank implementierten Logik ist nicht sinnvoll, und das Überprüfen des Testcodes würde dann zu einem Problem für sich.
Ich würde dies als ein Problem der nicht unbedingt behandeln Unit - Test vs integriert Test , sondern sehen den Umfang dessen , was getestet wird. Die allgemeinen Probleme beim Unit- und Integrationstest bleiben bestehen: Sie benötigen einen einigermaßen realistischen Satz von Testdaten und Testfällen, der jedoch auch so klein ist, dass die Tests schnell ausgeführt werden können.
Die Zeit zum Zurücksetzen der Datenbank und zum erneuten Auffüllen mit Testdaten ist ein zu berücksichtigender Aspekt. Sie würden dies im Allgemeinen anhand der Zeit bewerten, die erforderlich ist, um diesen Scheincode zu schreiben (den Sie schließlich auch pflegen müssten).
Ein weiterer zu berücksichtigender Punkt ist der Grad der Abhängigkeit Ihrer Anwendung von der Datenbank.
quelle
Sie halten einen solchen Komponententest zu Recht für unvollständig. Die Unvollständigkeit wird in der Datenbankschnittstelle verspottet. Solche naiven Scheinerwartungen oder Behauptungen sind unvollständig.
Um dies zu vervollständigen, müssten Sie genügend Zeit und Ressourcen für das Schreiben oder Integrieren einer SQL-Regelengine einplanen, die gewährleisten würde , dass die SQL-Anweisung, die vom Testobjekt ausgegeben wird, zu erwarteten Vorgängen führt.
Die häufig vergessene und etwas teure Alternative zum Verspotten ist jedoch die "Virtualisierung" .
Können Sie eine temporäre, speicherinterne, aber "echte" DB-Instanz zum Testen einer einzelnen Funktion starten? Ja ? Dort haben Sie einen besseren Test, der die tatsächlich gespeicherten und abgerufenen Daten überprüft.
Nun könnte man sagen, Sie haben einen Unit-Test in einen Integrationstest verwandelt. Es gibt unterschiedliche Ansichten darüber, wo die Grenze gezogen werden muss, um zwischen Komponententests und Integrationstests zu klassifizieren. IMHO ist "Einheit" eine willkürliche Definition und sollte Ihren Bedürfnissen entsprechen.
quelle
Unit Tests
undIntegration Tests
sind orthgonal zueinander. Sie bieten eine andere Sicht auf die Anwendung, die Sie erstellen. Normalerweise möchten Sie beides . Der Zeitpunkt ist jedoch unterschiedlich, wann Sie welche Art von Tests wünschen.Am häufigsten möchten Sie
Unit Tests
. Unit-Tests konzentrieren sich auf einen kleinen Teil des zu testenden Codes - was genau als a bezeichnetunit
wird, bleibt dem Leser überlassen. Das Ziel ist jedoch einfach: Sie erhalten schnelles Feedback, wann und wo Ihr Code kaputt gegangen ist . Das heißt, es sollte klar sein, dass Aufrufe an eine tatsächliche DB ein Nono sind .Andererseits gibt es Dinge, die nur unter harten Bedingungen ohne Datenbank getestet werden können. Möglicherweise enthält Ihr Code eine Race-Bedingung , und ein Aufruf einer DB führt zu einer Verletzung von a,
unique constraint
die nur ausgelöst werden kann, wenn Sie Ihr System tatsächlich verwenden. Aber solche Tests sind teuer, und Sie können (und wollen) sie nicht so oft ausführenunit tests
.quelle
In der .Net-Welt habe ich die Angewohnheit, ein Testprojekt und Tests als Methode zum Codieren / Debuggen / Testen von Roundtrips abzüglich der Benutzeroberfläche zu erstellen. Dies ist ein effizienter Weg für mich, mich zu entwickeln. Ich war nicht so sehr daran interessiert, alle Tests für jeden Build durchzuführen (da dies meinen Entwicklungsworkflow verlangsamt), aber ich verstehe, wie nützlich dies für ein größeres Team ist. Sie können jedoch die Regel festlegen, dass vor dem Festschreiben des Codes alle Tests ausgeführt werden und bestanden werden sollten (wenn die Ausführung der Tests länger dauert, da die Datenbank tatsächlich betroffen ist).
Das Verspotten der Datenzugriffsschicht (DAO) und das Nichttreffen auf der Datenbank ermöglicht es mir nicht nur, so zu codieren, wie ich es mag und wie ich es gewöhnt bin, sondern es fehlt auch ein großer Teil der eigentlichen Codebasis. Wenn Sie die Datenzugriffsebene und die Datenbank nicht wirklich testen und nur so tun, als würden Sie viel Zeit damit verbringen, Dinge zu verspotten, kann ich den Nutzen dieses Ansatzes nicht erfassen, um meinen Code tatsächlich zu testen. Ich teste ein kleines Stück anstelle eines größeren mit einem Test. Ich verstehe, dass mein Ansatz eher im Sinne eines Integrationstests ist, aber es scheint, dass der Unit-Test mit dem Mock eine überflüssige Zeitverschwendung ist, wenn Sie den Integrationstest tatsächlich nur einmal und zuerst schreiben. Es ist auch eine gute Möglichkeit zum Entwickeln und Debuggen.
Tatsächlich kenne ich TDD und Behaviour Driven Design (BDD) schon seit einiger Zeit und denke darüber nach, wie ich es verwenden kann, aber es ist schwierig, Komponententests rückwirkend hinzuzufügen. Vielleicht irre ich mich, aber das Schreiben eines Tests, der mehr Code von Ende zu Ende mit der enthaltenen Datenbank abdeckt, scheint ein viel vollständigerer Test mit höherer Priorität zu sein, der mehr Code abdeckt und eine effizientere Methode zum Schreiben von Tests darstellt.
Tatsächlich denke ich, dass so etwas wie Behavior Driven Design (BDD), das End-to-End-Tests mit domänenspezifischer Sprache (DSL) durchführt, der richtige Weg sein sollte. Wir haben SpecFlow in der .Net-Welt, aber es begann als Open Source mit Cucumber.
https://cucumber.io/
Ich bin einfach nicht wirklich beeindruckt von der wahren Nützlichkeit des Tests, den ich geschrieben habe, indem ich die Datenzugriffsebene verspottet habe und nicht auf die Datenbank zugegriffen habe. Das zurückgegebene Objekt hat die Datenbank nicht erreicht und wurde nicht mit Daten gefüllt. Es war ein völlig leeres Objekt, das ich auf unnatürliche Weise verspotten musste. Ich denke nur, es ist Zeitverschwendung.
Laut Stack Overflow wird Mocking verwendet, wenn es unpraktisch ist, reale Objekte in den Komponententest einzubeziehen.
https://stackoverflow.com/questions/2665812/what-is-mocking
"Mocking wird hauptsächlich beim Komponententest verwendet. Ein zu testendes Objekt kann Abhängigkeiten von anderen (komplexen) Objekten aufweisen. Um das Verhalten des zu testenden Objekts einzugrenzen, ersetzen Sie die anderen Objekte durch Mocks, die das Verhalten der realen Objekte simulieren. Dies ist nützlich, wenn es unpraktisch ist, die realen Objekte in den Komponententest einzubeziehen. "
Mein Argument ist, dass ich diesen Roundtrip-Ablauf testen werde, bevor ich als Entwickler etwas einchecke, wenn ich irgendetwas von Ende zu Ende codiere (Web-Benutzeroberfläche zu Business-Schicht zu Datenzugriffsschicht zu Datenbank, Roundtrip). Wenn ich die Benutzeroberfläche ausschneide und diesen Ablauf ausgehend von einem Test debugge und teste, teste ich alles außerhalb der Benutzeroberfläche und gebe genau das zurück, was die Benutzeroberfläche erwartet. Ich muss nur noch die Benutzeroberfläche senden, was sie will.
Ich habe einen vollständigeren Test, der Teil meines natürlichen Entwicklungs-Workflows ist. Für mich sollte dies der Test mit der höchsten Priorität sein, der das Testen der tatsächlichen Benutzerspezifikation von Ende zu Ende so weit wie möglich umfasst. Wenn ich noch nie einen detaillierteren Test erstellt habe, habe ich zumindest diesen vollständigen Test, der beweist, dass meine gewünschte Funktionalität funktioniert.
Ein Mitbegründer von Stack Exchange ist nicht überzeugt von den Vorteilen einer 100-prozentigen Testabdeckung. Ich bin es auch nicht. Ich würde einen vollständigeren "Integrationstest" machen, der die Datenbank überfordert, jeden Tag eine Reihe von Datenbank-Mocks zu warten.
https://www.joelonsoftware.com/2009/01/31/from-podcast-38/
quelle
Externe Abhängigkeiten sollten verspottet werden, da Sie sie nicht kontrollieren können (sie können während der Integrationstestphase bestehen, aber in der Produktion scheitern). Laufwerke können ausfallen, Datenbankverbindungen können aus einer Reihe von Gründen ausfallen, es können Netzwerkprobleme auftreten usw. Integrationstests geben kein zusätzliches Vertrauen, da sie alle Probleme darstellen, die zur Laufzeit auftreten können.
Mit echten Komponententests testen Sie innerhalb der Grenzen der Sandbox und es sollte klar sein. Wenn ein Entwickler eine SQL-Abfrage geschrieben hat, die in QA / PROD fehlgeschlagen ist, bedeutet dies, dass er sie vorher nicht einmal getestet hat.
quelle