Was ist die beste Strategie zum Testen datenbankgesteuerter Anwendungen?

346

Ich arbeite mit vielen Webanwendungen, die von Datenbanken unterschiedlicher Komplexität im Backend gesteuert werden. In der Regel gibt es eine ORM- Schicht, die von der Geschäfts- und Präsentationslogik getrennt ist. Dies macht das Unit-Testen der Geschäftslogik ziemlich einfach; Dinge können in diskreten Modulen implementiert werden und alle Daten, die für den Test benötigt werden, können durch Objektverspottung gefälscht werden.

Das Testen des ORM und der Datenbank selbst war jedoch immer mit Problemen und Kompromissen behaftet.

Im Laufe der Jahre habe ich einige Strategien ausprobiert, von denen mich keine vollständig zufriedenstellte.

  • Laden Sie eine Testdatenbank mit bekannten Daten. Führen Sie Tests gegen das ORM durch und bestätigen Sie, dass die richtigen Daten zurückkommen. Der Nachteil hierbei ist, dass Ihre Test-DB mit allen Schemaänderungen in der Anwendungsdatenbank Schritt halten muss und möglicherweise nicht mehr synchron ist. Es basiert auch auf künstlichen Daten und deckt möglicherweise keine Fehler auf, die aufgrund dummer Benutzereingaben auftreten. Wenn die Testdatenbank klein ist, werden keine Ineffizienzen wie ein fehlender Index angezeigt. (OK, das letzte ist nicht wirklich das, wofür Unit-Tests verwendet werden sollten, aber es tut nicht weh.)

  • Laden Sie eine Kopie der Produktionsdatenbank und testen Sie diese. Das Problem hierbei ist, dass Sie möglicherweise zu keinem Zeitpunkt eine Ahnung haben, was sich in der Produktionsdatenbank befindet. Ihre Tests müssen möglicherweise neu geschrieben werden, wenn sich die Daten im Laufe der Zeit ändern.

Einige Leute haben darauf hingewiesen, dass beide Strategien auf bestimmten Daten beruhen und ein Komponententest nur die Funktionalität testen sollte. Zu diesem Zweck habe ich vorgeschlagen gesehen:

  • Verwenden Sie einen nachgebildeten Datenbankserver und überprüfen Sie nur, ob der ORM die richtigen Abfragen als Antwort auf einen bestimmten Methodenaufruf sendet.

Welche Strategien haben Sie zum Testen datenbankgesteuerter Anwendungen verwendet, falls vorhanden? Was hat bei Ihnen am besten funktioniert?

Friedo
quelle
Ich denke, Sie sollten immer noch Datenbankindizes in einer Testumgebung für Fälle wie eindeutige Indizes haben.
dtc
Ich habe nichts gegen diese Frage hier, aber wenn wir uns an die Regeln halten, ist diese Frage nicht für den Stackoverflow, sondern für die Website softwareengineering.stackexchange .
ITExpert

Antworten:

155

Ich habe Ihren ersten Ansatz tatsächlich mit einigem Erfolg verwendet, aber auf eine etwas andere Art und Weise, die meiner Meinung nach einige Ihrer Probleme lösen würde:

  1. Behalten Sie das gesamte Schema und die Skripte zum Erstellen in der Quellcodeverwaltung bei, damit jeder nach dem Auschecken das aktuelle Datenbankschema erstellen kann. Bewahren Sie außerdem Beispieldaten in Datendateien auf, die von einem Teil des Erstellungsprozesses geladen werden. Wenn Sie Daten entdecken, die Fehler verursachen, fügen Sie sie Ihren Beispieldaten hinzu, um zu überprüfen, ob Fehler nicht erneut auftreten.

  2. Verwenden Sie einen Continuous Integration Server, um das Datenbankschema zu erstellen, die Beispieldaten zu laden und Tests auszuführen. Auf diese Weise halten wir unsere Testdatenbank synchron (erstellen sie bei jedem Testlauf neu). Obwohl dies erfordert, dass der CI-Server Zugriff und Besitz seiner eigenen dedizierten Datenbankinstanz hat, hat die dreimalige Erstellung unseres Datenbankschemas dazu beigetragen, Fehler zu finden, die wahrscheinlich erst kurz vor der Auslieferung (wenn nicht später) gefunden worden wären ). Ich kann nicht sagen, dass ich das Schema vor jedem Commit neu erstelle. Hat jemand? Mit diesem Ansatz müssen Sie nicht (na ja, vielleicht sollten wir, aber es ist keine große Sache, wenn jemand vergisst).

  3. Für meine Gruppe erfolgt die Benutzereingabe auf Anwendungsebene (nicht db), sodass dies über Standard-Unit-Tests getestet wird.

Laden der Produktionsdatenbankkopie:
Dies war der Ansatz, der bei meinem letzten Job verwendet wurde. Es war eine große Schmerzursache für ein paar Probleme:

  1. Die Kopie würde aus der Produktionsversion veraltet sein
  2. Änderungen würden am Schema der Kopie vorgenommen und nicht an die Produktionssysteme weitergegeben. Zu diesem Zeitpunkt hätten wir unterschiedliche Schemata. Kein Spaß.

Verspottender Datenbankserver:
Wir machen das auch bei meinem aktuellen Job. Nach jedem Commit führen wir Unit-Tests für den Anwendungscode durch, in den Schein-DB-Accessoren injiziert wurden. Dann führen wir dreimal am Tag den oben beschriebenen vollständigen Datenbank-Build aus. Ich empfehle definitiv beide Ansätze.

Mark Roddy
quelle
37
Das Laden einer Produktionsdatenbankkopie hat auch Auswirkungen auf die Sicherheit und den Datenschutz. Sobald es groß wird, kann es eine große Sache sein, eine Kopie davon zu nehmen und sie in Ihre Entwicklungsumgebung zu stellen.
WW.
Ehrlich gesagt ist dies ein großer Schmerz. Ich bin neu im Testen und ich habe auch einen Orm geschrieben, den ich testen möchte. Ich habe bereits Ihre erste Methode verwendet, aber gelesen, dass sie die Testeinheit nicht macht. Ich verwende bestimmte Funktionen der DB-Engine und daher wird es schwierig sein, ein DAO zu verspotten. Ich denke, ich verwende nur meine aktuelle Methode, da sie funktioniert und andere sie verwenden. Automatisierte Tests rocken übrigens. Vielen Dank.
frostymarvelous
2
Ich verwalte zwei verschiedene große Projekte, in einem von ihnen war dieser Ansatz perfekt, aber wir hatten viele Probleme, dies im anderen Projekt umzusetzen. Ich denke, das hängt davon ab, wie einfach das Schema jedes Mal neu erstellt werden kann, um die Tests auszuführen. Derzeit arbeite ich daran, eine neue Lösung für dieses immer letzte Problem zu finden.
Kreuz
2
In diesem Fall lohnt es sich auf jeden Fall, ein Datenbankversionierungstool wie Roundhouse zu verwenden, mit dem Migrationen ausgeführt werden können. Dies kann auf jeder DB-Instanz ausgeführt werden und sollte sicherstellen, dass die Schemas auf dem neuesten Stand sind. Darüber hinaus sollten beim Schreiben von Migrationsskripten auch Testdaten geschrieben werden, um Migrationen und Daten synchron zu halten.
jedd.ahyoung
Verwenden Sie besser das Patchen und Verspotten von Affen und vermeiden Sie Schreibvorgänge
Nickpick
56

Ich führe aus folgenden Gründen immer Tests gegen eine In-Memory-Datenbank (HSQLDB oder Derby) durch:

  • Sie überlegen, welche Daten in Ihrer Test-DB gespeichert werden sollen und warum. Nur Ihre Produktions-DB in ein Testsystem zu ziehen, bedeutet: "Ich habe keine Ahnung, was ich tue oder warum und wenn etwas kaputt geht, war ich es nicht !!" ;)
  • Es stellt sicher, dass die Datenbank mit geringem Aufwand an einem neuen Ort neu erstellt werden kann (z. B. wenn ein Fehler aus der Produktion repliziert werden muss).
  • Es hilft enorm bei der Qualität der DDL-Dateien.

Die In-Memory-Datenbank wird nach dem Start der Tests mit neuen Daten geladen. Nach den meisten Tests rufe ich ROLLBACK auf, um sie stabil zu halten. IMMERHalten Sie die Daten in der Test-DB stabil! Wenn sich die Daten ständig ändern, können Sie nicht testen.

Die Daten werden aus SQL, einer Vorlagen-DB oder einem Dump / Backup geladen. Ich bevorzuge Dumps, wenn sie in einem lesbaren Format vorliegen, da ich sie in VCS einfügen kann. Wenn das nicht funktioniert, verwende ich eine CSV-Datei oder XML. Wenn ich enorme Datenmengen laden muss ... tue ich nicht. Sie müssen nie enorme Datenmengen laden :) Nicht für Unit-Tests. Leistungstests sind ein weiteres Problem, und es gelten andere Regeln.

Aaron Digulla
quelle
1
Ist Geschwindigkeit der einzige Grund für die Verwendung (speziell) einer speicherinternen Datenbank?
Rinogo
2
Ich denke, ein weiterer Vorteil könnte sein "Wegwerf" -Natur sein - keine Notwendigkeit, nach sich selbst aufzuräumen; Töte einfach die In-Memory-DB. (Aber es gibt andere Möglichkeiten, dies zu erreichen, wie den von Ihnen erwähnten
ROLLBACK-
1
Der Vorteil ist, dass jeder Test seine Strategie individuell auswählen kann. Wir haben Tests, die die Arbeit in untergeordneten Threads erledigen, was bedeutet, dass Spring die Daten immer festschreibt.
Aaron Digulla
@ Aaron: Wir verfolgen auch diese Strategie. Ich würde gerne wissen, wie Sie vorgehen, um zu behaupten, dass das In-Memory-Modell dieselbe Struktur hat wie die echte Datenbank.
Guillaume
1
@ Guillaume: Ich erstelle alle Datenbanken aus den gleichen SQL-Dateien. H2 eignet sich hervorragend dafür, da es die meisten SQL-Besonderheiten der wichtigsten Datenbanken unterstützt. Wenn das nicht funktioniert, verwende ich einen Filter, der das ursprüngliche SQL verwendet und dieses in das SQL für die In-Memory-Datenbank konvertiert.
Aaron Digulla
14

Ich habe diese Frage schon lange gestellt, aber ich denke, dafür gibt es keine Silberkugel.

Was ich derzeit mache, ist das Verspotten der DAO-Objekte und das Speichern einer guten Sammlung von Objekten im Speicher, die interessante Fälle von Daten darstellen, die in der Datenbank leben könnten.

Das Hauptproblem, das ich bei diesem Ansatz sehe, besteht darin, dass Sie nur den Code abdecken, der mit Ihrer DAO-Ebene interagiert, aber niemals das DAO selbst testen, und meiner Erfahrung nach treten auch auf dieser Ebene viele Fehler auf. Ich führe auch einige Komponententests durch, die für die Datenbank ausgeführt werden (um TDD zu verwenden oder schnell lokal zu testen), aber diese Tests werden niemals auf meinem Server für kontinuierliche Integration ausgeführt, da wir zu diesem Zweck keine Datenbank führen und ich Denken Sie, dass Tests, die auf dem CI-Server ausgeführt werden, in sich geschlossen sein sollten.

Ein anderer Ansatz, den ich sehr interessant finde, der sich jedoch nicht immer lohnt, da er etwas zeitaufwändig ist, besteht darin, dasselbe Schema zu erstellen, das Sie für die Produktion in einer eingebetteten Datenbank verwenden, die nur innerhalb des Komponententests ausgeführt wird.

Obwohl es keine Frage gibt, dass dieser Ansatz Ihre Abdeckung verbessert, gibt es einige Nachteile, da Sie so nah wie möglich an ANSI SQL sein müssen, damit es sowohl mit Ihrem aktuellen DBMS als auch mit dem eingebetteten Ersatz funktioniert.

Unabhängig davon, was Ihrer Meinung nach für Ihren Code relevanter ist, gibt es einige Projekte, die es möglicherweise einfacher machen, wie DbUnit .

kolrie
quelle
13

Auch wenn es Tools, die es Ihnen ermöglichen , Ihre Datenbank in eine oder andere Weise zu verspotten (zB jOOQ ‚s MockConnection, die in zu sehen ist diese Antwort - Disclaimer, die ich für jOOQ des Verkäufers arbeiten), würde ich raten , nicht größere Datenbanken mit komplexen zu verspotten Anfragen.

Auch wenn Sie Ihren ORM nur auf Integrationstest testen möchten, achten Sie darauf, dass ein ORM eine sehr komplexe Reihe von Abfragen an Ihre Datenbank ausgibt, die sich darin unterscheiden können

  • Syntax
  • Komplexität
  • Auftrag (!)

Das alles zu verspotten, um vernünftige Dummy-Daten zu erzeugen, ist ziemlich schwierig, es sei denn, Sie erstellen tatsächlich eine kleine Datenbank in Ihrem Mock, die die übertragenen SQL-Anweisungen interpretiert. Verwenden Sie jedoch eine bekannte Integrationstest-Datenbank, die Sie problemlos mit bekannten Daten zurücksetzen können, anhand derer Sie Ihre Integrationstests ausführen können.

Lukas Eder
quelle
5

Ich benutze die erste (Ausführen des Codes gegen eine Testdatenbank). Das einzige wesentliche Problem, das Sie bei diesem Ansatz ansprechen, ist die Möglichkeit, dass Schemas nicht mehr synchron sind. Ich behebe dies, indem ich eine Versionsnummer in meiner Datenbank behalte und alle Schemaänderungen über ein Skript vornehme, das die Änderungen für jedes Versionsinkrement anwendet.

Ich nehme auch zuerst alle Änderungen (einschließlich des Datenbankschemas) an meiner Testumgebung vor, sodass es umgekehrt ist: Wenden Sie nach Abschluss aller Tests die Schemaaktualisierungen auf den Produktionshost an. Ich habe auch ein separates Paar von Test- und Anwendungsdatenbanken auf meinem Entwicklungssystem, damit ich dort überprüfen kann, ob das Datenbank-Upgrade ordnungsgemäß funktioniert, bevor ich die realen Produktionsboxen berühre.

Dave Sherohman
quelle
3

Ich verwende den ersten Ansatz, aber etwas anders, um die von Ihnen genannten Probleme anzugehen.

Alles, was zum Ausführen von Tests für DAOs benötigt wird, befindet sich in der Quellcodeverwaltung. Es enthält ein Schema und Skripte zum Erstellen der Datenbank (Docker ist dafür sehr gut geeignet). Wenn die eingebettete Datenbank verwendet werden kann, verwende ich sie aus Gründen der Geschwindigkeit.

Der wichtige Unterschied zu den anderen beschriebenen Ansätzen besteht darin, dass die für den Test erforderlichen Daten nicht aus SQL-Skripten oder XML-Dateien geladen werden. Alles (mit Ausnahme einiger Wörterbuchdaten, die effektiv konstant sind) wird von der Anwendung mithilfe von Dienstprogrammfunktionen / -klassen erstellt.

Der Hauptzweck besteht darin, Daten für Tests zu verwenden

  1. sehr nah am Test
  2. explizit (die Verwendung von SQL-Dateien für Daten macht es sehr problematisch zu sehen, welche Daten von welchem ​​Test verwendet werden)
  3. Isolieren Sie Tests von den nicht verwandten Änderungen.

Dies bedeutet im Grunde, dass diese Dienstprogramme es ermöglichen, nur Dinge, die für den Test im Test selbst wesentlich sind, deklarativ anzugeben und irrelevante Dinge wegzulassen.

Um eine Vorstellung davon zu bekommen, was es in der Praxis bedeutet, betrachten Sie den Test für ein DAO, das mit Comments bis Posts arbeitet, die von geschrieben wurden Authors. Um CRUD-Operationen für ein solches DAO zu testen, sollten einige Daten in der Datenbank erstellt werden. Der Test würde so aussehen:

@Test
public void savedCommentCanBeRead() {
    // Builder is needed to declaratively specify the entity with all attributes relevant
    // for this specific test
    // Missing attributes are generated with reasonable values
    // factory's responsibility is to create entity (and all entities required by it
    //  in our example Author) in the DB
    Post post = factory.create(PostBuilder.post());

    Comment comment = CommentBuilder.comment().forPost(post).build();

    sut.save(comment);

    Comment savedComment = sut.get(comment.getId());

    // this checks fields that are directly stored
    assertThat(saveComment, fieldwiseEqualTo(comment));
    // if there are some fields that are generated during save check them separately
    assertThat(saveComment.getGeneratedField(), equalTo(expectedValue));        
}

Dies hat mehrere Vorteile gegenüber SQL-Skripten oder XML-Dateien mit Testdaten:

  1. Das Verwalten des Codes ist viel einfacher (das Hinzufügen einer obligatorischen Spalte, beispielsweise in einer Entität, auf die in vielen Tests verwiesen wird, wie z. B. Author, erfordert nicht das Ändern vieler Dateien / Datensätze, sondern nur eine Änderung des Builders und / oder der Factory)
  2. Die für einen bestimmten Test erforderlichen Daten werden im Test selbst und nicht in einer anderen Datei beschrieben. Diese Nähe ist sehr wichtig für die Verständlichkeit der Tests.

Rollback gegen Commit

Ich finde es bequemer, dass Tests festgeschrieben werden, wenn sie ausgeführt werden. Erstens einige Effekte (zum BeispielDEFERRED CONSTRAINTS ) nicht überprüft werden, wenn ein Commit nie stattfindet. Zweitens können die Daten, wenn ein Test fehlschlägt, in der Datenbank überprüft werden, da sie durch das Rollback nicht zurückgesetzt werden.

Dies hat natürlich den Nachteil, dass der Test zu fehlerhaften Daten führen kann und dies zu Fehlern bei anderen Tests führt. Um damit fertig zu werden, versuche ich die Tests zu isolieren. Im obigen Beispiel kann jeder Test neue erstellen Authorund alle anderen Entitäten werden im Zusammenhang damit erstellt, sodass Kollisionen selten sind. Um mit den verbleibenden Invarianten umzugehen, die möglicherweise beschädigt werden können, aber nicht als Einschränkung auf DB-Ebene ausgedrückt werden können, verwende ich einige programmatische Überprüfungen auf fehlerhafte Bedingungen, die nach jedem einzelnen Test ausgeführt werden können (und die in CI ausgeführt werden, aber normalerweise aus Leistungsgründen lokal ausgeschaltet sind Gründe dafür).

Roman Konoval
quelle
Wenn Sie die Datenbank mit Entitäten und dem Orm anstelle von SQL-Skripten erstellen, hat dies auch den Vorteil, dass der Compiler Sie zwingt, den Startcode zu korrigieren, wenn Sie Änderungen an Ihrem Modell vornehmen. Nur relevant, wenn Sie natürlich eine statisch typisierte Sprache verwenden.
Daramasala
Zur Verdeutlichung: Verwenden Sie die Dienstprogrammfunktionen / -klassen in Ihrer gesamten Anwendung oder nur für Ihre Tests?
Ella
@ Ella Diese Dienstprogrammfunktionen werden normalerweise nicht außerhalb des Testcodes benötigt. Denken Sie zum Beispiel darüber nach PostBuilder.post(). Es werden einige Werte für alle obligatorischen Attribute des Beitrags generiert. Dies wird im Produktionscode nicht benötigt.
Roman Konoval
2

Für JDBC-basierte Projekte (direkt oder indirekt, z. B. JPA, EJB, ...) können Sie nicht die gesamte Datenbank modellieren (in diesem Fall ist es besser, eine Test-Datenbank auf einem echten RDBMS zu verwenden), sondern nur ein Modell auf JDBC-Ebene .

Vorteil ist die Abstraktion, die auf diese Weise entsteht, da JDBC-Daten (Ergebnismenge, Aktualisierungsanzahl, Warnung, ...) unabhängig vom Backend gleich sind: Ihre Produktdatenbank, eine Testdatenbank oder nur einige Modelldaten, die für jeden Test bereitgestellt werden Fall.

Wenn die JDBC-Verbindung für jeden Fall nachgebildet ist, muss die Test-Datenbank nicht verwaltet werden (Bereinigung, jeweils nur ein Test, Nachladen von Fixtures, ...). Jede Modellverbindung ist isoliert und muss nicht bereinigt werden. In jedem Testfall werden nur minimal erforderliche Vorrichtungen bereitgestellt, um den JDBC-Austausch zu verspotten, wodurch die Komplexität der Verwaltung einer gesamten Testdatenbank vermieden wird.

Acolyte ist mein Framework, das einen JDBC-Treiber und ein Dienstprogramm für diese Art von Modell enthält: http://acolyte.eu.org .

cchantep
quelle