Unit-Tests und Datenbanken: Ab wann stelle ich tatsächlich eine Verbindung zur Datenbank her?

37

Es gibt Antworten auf die Frage, wie Testklassen eine Verbindung zu einer Datenbank herstellen, z. B. "Sollte Service-Testklassen eine Verbindung herstellen ..." und "Komponententest - Datenbankgekoppelte App" .

Nehmen wir also kurz an, Sie haben eine Klasse A, die eine Verbindung zu einer Datenbank herstellen muss. Anstatt A eine Verbindung herstellen zu lassen, stellen Sie A eine Schnittstelle zur Verfügung, über die A eine Verbindung herstellen kann. Zum Testen implementieren Sie diese Schnittstelle mit ein paar Dingen - natürlich ohne Verbindung. Wenn Klasse B A instanziiert, muss sie eine "echte" Datenbankverbindung an A übergeben. Dies bedeutet jedoch, dass B eine Datenbankverbindung öffnet. Das bedeutet, dass Sie zum Testen von B die Verbindung in B einspeisen. B wird jedoch in Klasse C und so weiter instanziiert.

Ab wann muss ich also sagen, dass ich hier Daten aus einer Datenbank abrufe und keinen Komponententest für diesen Code schreibe?

Mit anderen Worten: Irgendwo im Code einer Klasse muss ich mich melden sqlDB.connect()oder ähnliches. Wie teste ich diese Klasse?

Und ist es dasselbe mit Code, der mit einer GUI oder einem Dateisystem zu tun hat?


Ich möchte einen Unit-Test machen. Jede andere Art von Test hat nichts mit meiner Frage zu tun. Ich weiß, dass ich nur eine Klasse damit testen werde (da stimme ich dir zu, Kilian). Jetzt muss eine Klasse eine Verbindung zu einer Datenbank herstellen. Wenn ich diese Klasse testen und fragen möchte, wie ich das mache, sagen viele: "Use Dependency Injection!" Aber das verschiebt das Problem nur auf eine andere Klasse, nicht wahr? Also frage ich, wie teste ich die Klasse, die wirklich die Verbindung herstellt?

Bonusfrage: Einige Antworten beschränken sich auf "Use mock objects!" Was bedeutet das? Ich verspotte Klassen, von denen die getesteten Klassen abhängen. Soll ich den Prüfling jetzt verspotten und ihn tatsächlich testen (was der Idee der Verwendung von Vorlagenmethoden nahekommt, siehe unten)?

TobiMcNamobi
quelle
Ist es die Datenbankverbindung, die Sie testen? Wäre das Erstellen einer temporären Datenbank im Arbeitsspeicher (z. B. Derby ) akzeptabel?
@MichaelT Trotzdem muss ich die temporäre im Speicher befindliche DB durch eine echte Datenbank ersetzen. Woher? Wann? Wie wird das Gerät getestet? Oder ist es in Ordnung, diesen Code nicht zu testen?
TobiMcNamobi
3
Es gibt nichts zu "Unit-Test" über die Datenbank. Es wird von anderen Leuten gewartet, und wenn es einen Fehler geben würde, müssten Sie es von ihnen reparieren lassen, anstatt es selbst zu tun. Das einzige, was sich zwischen der tatsächlichen Verwendung Ihrer Klasse und der Verwendung während der Tests unterscheiden sollte, sollten die Parameter der Datenbankverbindung sein. Es ist unwahrscheinlich, dass der Code zum Lesen der Eigenschaftendatei oder der Spring-Injection-Mechanismus oder was auch immer Sie zum Zusammenweben Ihrer App verwenden, kaputt ist (und wenn dies der Fall wäre, könnten Sie es nicht selbst beheben, siehe oben) - ich halte es daher für akzeptabel Diesen Teil der Sanitärfunktionalität nicht zu testen.
Kilian Foth
2
@KilianFoth, das sich ausschließlich auf die Arbeitsumgebung und die Rollen der Mitarbeiter bezieht. Es hat eigentlich nichts mit der Frage zu tun. Was ist, wenn niemand für die Datenbank verantwortlich ist?
Reactgular
Mit einigen Mock-Frameworks können Sie Mock-Objekte in so ziemlich alles einfügen, selbst in private und statische Mitglieder. Dies macht das Testen mit Db-Verbindungen sooo viel einfacher. Mockito + Powermock funktioniert in diesen Tagen für mich (sie sind Java, nicht sicher, in was Sie arbeiten).
FrustratedWithFormsDesigner

Antworten:

21

Der Zweck eines Komponententests besteht darin, eine Klasse zu testen (in der Regel sollte eine Methode getestet werden ).

Dies bedeutet, dass Sie beim Testen einer Klasse Aeine Testdatenbank in die Testdatenbank einfügen - eine selbstgeschriebene oder eine blitzschnelle speicherinterne Datenbank, unabhängig davon, welche Aufgaben erledigt werden.

Wenn Sie jedoch eine Klasse testen B, bei der es sich um einen Client handelt A, verspotten Sie normalerweise das gesamte AObjekt mit etwas anderem, vermutlich etwas, das seine Aufgabe primitiv und vorprogrammiert erledigt - ohne Verwendung eines tatsächlichen AObjekts und mit Sicherheit ohne Verwendung von Daten base (es sei denn, Adie gesamte Datenbankverbindung wird an den Anrufer zurückgegeben - aber das ist so schrecklich, dass ich nicht darüber nachdenken möchte). Wenn Sie einen Komponententest für eine Klasse schreiben C, die ein Kunde von ist B, verspotten Sie ebenfalls etwas, das die Rolle von übernimmt B, und vergessen es Ainsgesamt.

Wenn Sie das nicht tun, handelt es sich nicht mehr um einen Komponententest, sondern um einen System- oder Integrationstest. Das ist auch sehr wichtig, aber ein ganz anderer Fischkessel. Zunächst sind sie in der Regel aufwändiger einzurichten und auszuführen. Es ist nicht praktikabel, die Weitergabe dieser Informationen als Voraussetzung für das Einchecken usw. zu verlangen.

Kilian Foth
quelle
11

Unit-Tests für eine Datenbankverbindung durchzuführen, ist völlig normal und eine gängige Praxis. Es ist einfach nicht möglich, einen puristAnsatz zu erstellen, bei dem alles in Ihrem System in Abhängigkeit injizierbar ist.

Der Schlüssel besteht darin, eine temporäre oder eine reine Testdatenbank zu testen und einen möglichst einfachen Startprozess für die Erstellung dieser Testdatenbank zu haben.

Für Unit-Tests in CakePHP gibt es Dinge, die man nennt fixtures. Fixtures sind temporäre Datenbanktabellen, die im laufenden Betrieb für einen Unit-Test erstellt werden. Das Gerät verfügt über praktische Methoden zum Erstellen. Sie können ein Schema aus einer Produktionsdatenbank in der Testdatenbank neu erstellen oder das Schema mit einer einfachen Notation definieren.

Der Schlüssel zum Erfolg besteht darin, nicht die Geschäftsdatenbank zu implementieren, sondern sich nur auf den Aspekt des Codes zu konzentrieren, den Sie testen. Wenn Sie über einen Komponententest verfügen, der bestätigt, dass ein Datenmodell nur veröffentlichte Dokumente liest, sollte das Tabellenschema für diesen Test nur die Felder enthalten, die für diesen Code erforderlich sind. Sie müssen nicht die gesamte Content-Management-Datenbank erneut implementieren, um diesen Code zu testen.

Einige zusätzliche Referenzen.

http://en.wikipedia.org/wiki/Test_fixture

http://phpunit.de/manual/3.7/en/database.html

http://book.cakephp.org/2.0/en/development/testing.html#fixtures

Reactgular
quelle
28
Ich stimme dir nicht zu. Ein Test, der eine Datenbankverbindung erfordert, ist kein Komponententest, da der Test von Natur aus Nebenwirkungen hat. Das bedeutet nicht, dass Sie keinen automatisierten Test schreiben können, aber ein solcher Test ist per Definition ein Integrationstest, der Bereiche Ihres Systems außerhalb Ihrer Codebasis ausübt.
KeithS
5
Nennen Sie mich einen Puristen, aber ich halte daran fest, dass ein Komponententest keine Aktionen ausführen sollte, die die "Sandbox" der Testlaufzeitumgebung verlassen. Sie sollten Datenbanken, Dateisysteme, Netzwerk-Sockets usw. nicht berühren. Dies hat mehrere Gründe, nicht zuletzt die Abhängigkeit des Tests vom externen Status. Ein anderer ist Leistung; Ihre Komponententestsuite sollte schnell ausgeführt werden, und die Verbindung mit diesen externen Datenspeichern verlangsamt die Tests um Größenordnungen. In meiner eigenen Entwicklung benutze ich teilweise Mocks, um Dinge wie meine Repositorys zu testen, und ich kann problemlos eine "Kante" zu meiner Sandbox definieren.
KeithS
2
@gbjbaanb - Das hört sich zunächst gut an, ist aber meiner Erfahrung nach sehr gefährlich. Selbst in den Testsuiten und Frameworks mit der besten Architektur wird der Code zum Zurücksetzen dieser Transaktion möglicherweise nicht ausgeführt. Wenn der Test-Runner innerhalb eines Tests abstürzt oder abgebrochen wird oder der Test ein SOE oder OOME ausgibt, besteht der beste Fall darin, dass Sie eine hängende Verbindung und Transaktion in der Datenbank haben, die die von Ihnen berührten Tabellen sperren, bis die Verbindung beendet wird. Die Art und Weise, wie Sie verhindern, dass dies zu Problemen führt, wie z. B. die Verwendung von SQLite als Test-DB, hat ihre eigenen Nachteile, z. B. dass Sie die echte DB nicht wirklich trainieren .
KeithS
5
@ KeithS Ich denke, wir diskutieren über Semantik. Es geht nicht darum, was die Definition eines Komponententests oder eines Integrationstests ist. Ich benutze Fixtures, um Code zu testen, der von einer Datenbankverbindung abhängt. Wenn das ein Integrationstest ist, bin ich damit einverstanden. Ich muss wissen, dass der Test bestanden wird. Ich konnte mich nicht um die Abhängigkeiten, die Leistung oder die Risiken kümmern. Ich werde nicht wissen, ob dieser Code funktioniert, wenn dieser Test nicht bestanden wird. Bei den meisten Tests gibt es keine Abhängigkeiten, aber bei denjenigen, bei denen es Abhängigkeiten gibt, können diese nicht entkoppelt werden. Es ist leicht zu sagen, dass sie es sein sollten, aber sie können es einfach nicht sein.
Reactgular
4
Ich denke, wir sind es auch. Ich verwende auch für meine Integrationstests ein "Unit-Testing-Framework" (NUnit), aber ich achte darauf, diese beiden Testkategorien (oft in separaten Bibliotheken) zu trennen. Der Punkt, den ich anstrebte, ist, dass Ihre Unit-Test-Suite, die Sie mehrmals am Tag vor jedem Check-in ausführen, während Sie der iterativen Rot-Grün-Refaktor-Methode folgen, vollständig isolierbar sein sollte, damit Sie sie ausführen können Diese Tests werden mehrmals täglich durchgeführt, ohne dass Sie Ihren Mitarbeitern auf die Zehen treten müssen.
KeithS
4

Irgendwo in Ihrer Codebasis befindet sich eine Codezeile, die die eigentliche Aktion zum Herstellen einer Verbindung mit der entfernten Datenbank ausführt. Diese Codezeile ist 9-mal in 10 ein Aufruf einer "eingebauten" Methode, die von den für Ihre Sprache und Umgebung spezifischen Laufzeitbibliotheken bereitgestellt wird. Als solches ist es nicht "Ihr" Code und Sie müssen ihn nicht testen. Für die Zwecke eines Komponententests können Sie darauf vertrauen, dass dieser Methodenaufruf ordnungsgemäß ausgeführt wird. Was Sie in Ihrer Unit-Testsuite noch testen können und sollten, sind Dinge wie die Sicherstellung, dass die Parameter, die für diesen Aufruf verwendet werden, Ihren Erwartungen entsprechen, z. B. die Richtigkeit der Verbindungszeichenfolge oder die SQL-Anweisung oder Name der gespeicherten Prozedur.

Dies ist einer der Gründe für die Einschränkung, dass Komponententests ihre Laufzeit- "Sandbox" nicht verlassen dürfen und vom externen Status abhängig sind. Es ist eigentlich ganz praktisch; Mit einem Komponententest soll überprüft werden, ob sich der von Ihnen geschriebene Code (oder der Code , der gerade in TDD geschrieben wird) so verhält, wie Sie es sich vorgestellt haben. Code, den Sie nicht geschrieben haben, wie z. B. die Bibliothek, mit der Sie Ihre Datenbankoperationen ausführen, sollte aus dem einfachen Grund, dass Sie ihn nicht geschrieben haben, nicht Bestandteil eines Komponententests sein.

In Ihrer Integrationstestsuite werden diese Einschränkungen gelockert. Jetzt Sie könnenDesigntests, die die Datenbank berühren, um sicherzustellen, dass der Code, den Sie geschrieben haben, gut mit Code zusammenarbeitet, den Sie nicht geschrieben haben. Diese beiden Testsuiten sollten jedoch getrennt bleiben, da Ihre Komponententestsuite umso effektiver ist, je schneller sie ausgeführt wird (sodass Sie schnell überprüfen können, ob alle Aussagen der Entwickler zu ihrem Code noch gültig sind) ist aufgrund der zusätzlichen Abhängigkeit von externen Ressourcen um Größenordnungen langsamer. Lassen Sie den Build-Bot alle paar Stunden Ihre vollständige Integrationssuite ausführen und die Tests ausführen, mit denen externe Ressourcen gesperrt werden, damit die Entwickler sich nicht gegenseitig auf die Nerven gehen, indem sie dieselben Tests lokal ausführen. Und wenn der Build kaputt geht, was dann? Es wird viel mehr Wert darauf gelegt, dass der Build-Bot niemals einen Build verfehlt, als es wahrscheinlich sein sollte.


Wie genau Sie sich nun daran halten können, hängt von Ihrer genauen Strategie ab, wie Sie eine Verbindung zur Datenbank herstellen und diese abfragen. In vielen Fällen, in denen Sie das "Bare-Bones" -Datenzugriffsframework verwenden müssen, z. B. bei den ADO.NET-Objekten SqlConnection und SqlStatement, kann eine von Ihnen entwickelte Methode aus integrierten Methodenaufrufen und anderem Code bestehen, der von a abhängig ist Datenbankverbindung. Das Beste, was Sie in dieser Situation tun können, ist, die gesamte Funktion zu verspotten und Ihren Integrationstestsuiten zu vertrauen. Es hängt auch davon ab, wie weit Sie bereit sind, Ihre Klassen so zu entwerfen, dass bestimmte Codezeilen zu Testzwecken ersetzt werden können (z. B. Tobis Vorschlag für das Template-Methodenmuster, das gut ist, weil es "partielle Verspottungen" ermöglicht).

Wenn sich Ihr Datenpersistenzmodell auf Code in Ihrer Datenschicht stützt (z. B. Trigger, gespeicherte Prozesse usw.), gibt es einfach keine andere Möglichkeit, Code auszuüben, den Sie selbst schreiben, als Tests zu entwickeln, die entweder in der Datenschicht ablaufen oder die Datenschicht durchqueren Grenze zwischen Ihrer Anwendungslaufzeit und dem DBMS. Ein Purist würde sagen, dass dieses Muster aus diesem Grund zugunsten eines ORM vermieden werden sollte. Ich glaube nicht, dass ich so weit kommen würde. Selbst im Zeitalter sprachintegrierter Abfragen und anderer vom Compiler überprüfter, domänenabhängiger Persistenzvorgänge sehe ich den Wert darin, die Datenbank nur auf die Vorgänge zu beschränken, die über gespeicherte Prozeduren verfügbar gemacht werden, und natürlich müssen solche gespeicherten Prozeduren automatisiert überprüft werden Tests. Aber solche Tests sind nicht Einheit Tests. Sie sind Integration Tests.

Wenn Sie ein Problem mit dieser Unterscheidung haben, liegt dies in der Regel an der hohen Bedeutung einer vollständigen "Codeabdeckung", auch bekannt als "Einheitentestabdeckung". Sie möchten sicherstellen, dass jede Codezeile von einem Komponententest abgedeckt wird. Ein edles Ziel im Gesicht, aber ich sage Hogwash; Diese Mentalität eignet sich für Anti-Patterns, die weit über diesen speziellen Fall hinausreichen, wie das Schreiben von aussagefreien Tests, die ausgeführt werden, aber keine Übungen ausführendein Code. Diese Arten von End-Runs, die ausschließlich der Deckung dienen, sind schädlicher als die Lockerung Ihrer Mindestdeckung. Wenn Sie sicherstellen möchten, dass jede Zeile Ihrer Codebasis durch einen automatisierten Test ausgeführt wird, ist dies ganz einfach. Berücksichtigen Sie beim Berechnen von Kennzahlen zur Codeabdeckung die Integrationstests. Sie könnten sogar noch einen Schritt weiter gehen und diese umstrittenen "Itino" -Tests ("Integration nur in Namen") isolieren, und zwischen Ihrer Unit-Testsuite und dieser Unterkategorie von Integrationstests (die immer noch relativ schnell ablaufen sollten) sollten Sie verdammt sein Nahezu vollständige Abdeckung.

KeithS
quelle
2

Unit-Tests sollten niemals eine Verbindung zu einer Datenbank herstellen. Per Definition sollten sie jeweils eine einzelne Codeeinheit (eine Methode) vollständig isoliert vom Rest Ihres Systems testen. Wenn dies nicht der Fall ist, handelt es sich nicht um einen Komponententest.

Abgesehen von der Semantik gibt es eine Vielzahl von Gründen, warum dies von Vorteil ist:

  • Tests laufen um Größenordnungen schneller
  • Die Rückkopplungsschleife wird sofort (<1s Rückkopplung für TDD als Beispiel)
  • Tests können für Build / Deploy-Systeme parallel ausgeführt werden
  • Tests benötigen keine Datenbank, um ausgeführt zu werden (vereinfacht oder beschleunigt das Erstellen erheblich)

Unit-Tests sind eine Möglichkeit, Ihre Arbeit zu überprüfen. Sie sollten alle Szenarien für eine bestimmte Methode skizzieren, dh normalerweise alle unterschiedlichen Pfade durch eine Methode. Es ist Ihre Spezifikation, auf die Sie aufbauen, ähnlich wie bei der doppelten Buchführung.

Was Sie beschreiben, ist eine andere Art von automatisiertem Test: ein Integrationstest. Während sie auch sehr wichtig sind, haben Sie im Idealfall viel weniger davon. Sie sollten überprüfen, ob eine Gruppe von Einheiten ordnungsgemäß ineinander integriert ist.

Wie testen Sie Dinge mit Datenbankzugriff? Der gesamte Datenzugriffscode sollte sich auf einer bestimmten Ebene befinden, damit der Anwendungscode mit nachahmbaren Diensten anstelle der eigentlichen Datenbank interagieren kann. Es sollte egal sein, ob diese Dienste von einer SQL-Datenbank, In-Memory-Testdaten oder sogar von Remote-Webservice-Daten unterstützt werden. Das ist nicht ihre Sorge.

Im Idealfall (und das ist sehr subjektiv) möchten Sie, dass der Großteil Ihres Codes durch Unit-Tests abgedeckt wird. Dies gibt Ihnen die Gewissheit, dass jedes Stück unabhängig arbeitet. Sobald die Stücke gebaut sind, müssen Sie sie zusammenfügen. Beispiel - Wenn ich das Passwort des Benutzers hashe, sollte ich genau diese Ausgabe erhalten.

Angenommen, jede Komponente besteht aus ungefähr 5 Klassen - Sie möchten alle darin enthaltenen Fehlerquellen testen. Dies entspricht viel weniger Tests, nur um sicherzustellen, dass alles richtig verdrahtet ist. Beispiel - Test können Sie den Benutzer aus der Datenbank mit einem Benutzernamen / Passwort finden.

Schließlich möchten Sie einige Abnahmetests durchführen, um sicherzustellen, dass Sie die Geschäftsziele erreichen. Es gibt noch weniger davon; Sie stellen möglicherweise sicher, dass die Anwendung ausgeführt wird und das tut, wofür sie erstellt wurde. Beispiel - Angesichts dieser Testdaten sollte ich mich einloggen können.

Stellen Sie sich diese drei Arten von Tests als Pyramide vor. Sie brauchen eine Menge Unit-Tests, um alles zu unterstützen, und dann arbeiten Sie sich von dort nach oben.

Adrian Schneider
quelle
1

Das Template Method Pattern könnte helfen.

Sie verpacken die Aufrufe einer Datenbank in protectedMethoden. Um diese Klasse zu testen, testen Sie ein falsches Objekt, das von der realen Datenbankverbindungsklasse erbt und die geschützten Methoden überschreibt.

Auf diese Weise werden die tatsächlichen Aufrufe der Datenbank niemals einem Komponententest unterzogen, das ist richtig. Aber es sind nur diese wenigen Codezeilen. Und das ist akzeptabel.

TobiMcNamobi
quelle
1
Falls Sie sich fragen, warum ich meine eigene Frage beantworte: Ja, dies könnte eine Antwort sein, aber ich bin mir nicht sicher, ob es die richtige ist.
TobiMcNamobi
-1

Testen mit externen Daten ist ein Integrationstest. Einheitentest bedeutet, dass Sie nur die Einheit testen. Dies geschieht hauptsächlich mit Ihrer Geschäftslogik. Um Ihre Code-Einheit testbar zu machen, müssen Sie einige Richtlinien befolgen, z. B. müssen Sie Ihre Einheit von anderen Teilen Ihres Codes unabhängig machen. Wenn Sie während des Komponententests Daten benötigen, müssen Sie diese Daten mit Abhängigkeitsinjektion erzwingen. Es gibt einige spöttische und stumpfe Rahmenbedingungen da draußen.

DeveloperArnab
quelle