Java >> Java Tutorial >  >> Java

Schreiben von Tests für Datenzugriffscode - Vergessen Sie die Datenbank nicht

Wenn wir Tests für unseren Datenzugriffscode schreiben, müssen wir diese drei Regeln befolgen:

  1. Unsere Tests müssen das echte Datenbankschema verwenden.
  2. Unsere Tests müssen deterministisch sein.
  3. Unsere Tests müssen das Richtige bestätigen.

Diese Regeln sind offensichtlich .

Deshalb ist es überraschend, dass einige Entwickler sie kaputt machen (ich habe sie in der Vergangenheit auch kaputt gemacht).

Dieser Blogbeitrag beschreibt, warum diese Regeln wichtig sind und hilft uns, sie zu befolgen.

Regel 1:Wir müssen das echte Datenbankschema verwenden

Der zweite Teil dieser Serie hat uns gelehrt, dass wir unsere Integrationstests konfigurieren sollten, indem wir die gleiche Konfiguration verwenden, die von unserer Anwendung verwendet wird. Es hat uns auch gelehrt, dass es in Ordnung ist, diese Regel zu brechen, wenn wir einen guten Grund dafür haben.

Untersuchen wir eine recht häufige Situation, in der unsere Integrationstests eine andere Konfiguration als unsere Anwendung verwenden.

Wir können unsere Datenbank folgendermaßen erstellen:

  • Wir erstellen die Datenbank unserer Anwendung mit Liquibase. Wir verwenden die Spring-Integration, um die erforderlichen Änderungen an der Datenbank vorzunehmen, wenn die Anwendung gestartet wurde.
  • Wir lassen Hibernate die in unseren Integrationstests verwendete Datenbank erstellen.

Ich habe das auch gemacht, und es fühlte sich wie eine perfekte Lösung an, weil

  • Ich konnte die Vorteile einer versionierten Datenbank genießen.
  • Das Schreiben von Integrationstests fühlte sich wie ein Spaziergang im Park an, weil ich darauf vertrauen konnte, dass Hibernate eine funktionierende Datenbank für meine Integrationstests erstellt.

Nachdem ich jedoch begonnen habe, dieses Blog-Tutorial (Writing Tests for Data Access Code) zu schreiben, wurde mir klar, dass dieser Ansatz (mindestens) drei Probleme hat:

  • Wenn die Datenbank von Hibernate erstellt wird, können wir nicht testen, ob unsere Migrationsskripts eine funktionierende Datenbank erstellen.
  • Die von Hibernate erstellte Datenbank ist nicht unbedingt gleich der von unseren Migrationsskripten erstellten Datenbank. Wenn die Datenbank beispielsweise Tabellen enthält, die nicht als Entitäten beschrieben sind, erstellt Hibernate diese Tabellen (natürlich) nicht.
  • Wenn wir Leistungstests in der Integrationstestsuite ausführen möchten, müssen wir die erforderlichen Indizes mithilfe von @Index konfigurieren Anmerkung. Wenn wir dies nicht tun, erstellt Hibernate diese Indizes nicht. Das bedeutet, dass wir den Ergebnissen unserer Leistungstests nicht vertrauen können.

Sollten wir uns um diese Probleme kümmern?

Auf jeden Fall .

Wir dürfen nicht vergessen, dass jede testspezifische Änderung einen Unterschied zwischen unserer Testkonfiguration und der Produktionskonfiguration erzeugt. Wenn dieser Unterschied zu groß ist, sind unsere Tests wertlos.

Wenn wir unsere Integrationstests nicht mit demselben Datenbankschema ausführen, das verwendet wird, wenn die Anwendung in der Entwicklungs-/Test-/Produktionsumgebung bereitgestellt wird, stehen wir vor den folgenden Problemen:

  • Wir können nicht unbedingt Integrationstests für bestimmte Funktionen schreiben, da unserer Datenbank die erforderlichen Tabellen, Trigger, Einschränkungen oder Indizes fehlen. Das bedeutet, dass wir diese Funktionen manuell testen müssen, bevor die Anwendung in der Produktionsumgebung bereitgestellt wird. Das ist Zeitverschwendung.
  • Die Feedback-Schleife ist viel länger, als sie sein könnte, weil wir einige Probleme bemerken (z. B. Probleme, die durch fehlerhafte Migrationsskripts verursacht werden), nachdem die Anwendung in der Zielumgebung bereitgestellt wurde.
  • Wenn wir ein Problem bemerken, wenn eine Anwendung in einer Produktionsumgebung bereitgestellt wird, geht die Scheiße in die Luft und wir sind damit bedeckt. Ich mag es nicht, mit Kot bedeckt zu sein. Du?

Wenn wir diese Probleme vermeiden und die Vorteile unserer Datenzugriffstests maximieren möchten, müssen unsere Integrationstests dasselbe Datenbankschema verwenden, das verwendet wird, wenn unsere Anwendung in der Produktionsumgebung bereitgestellt wird.

Regel 2:Unsere Tests müssen deterministisch sein

Martin Fowler spezifiziert nicht-deterministische Tests wie folgt:

Ein Test ist nicht deterministisch, wenn er manchmal besteht und manchmal fehlschlägt, ohne dass sich der Code, die Tests oder die Umgebung merklich geändert haben. Solche Tests schlagen fehl, dann führen Sie sie erneut aus und sie bestehen. Testfehler bei solchen Tests sind scheinbar zufällig.

Er erklärt auch, warum nicht-deterministische Tests ein Problem darstellen:

Das Problem bei nicht deterministischen Tests ist, dass Sie, wenn sie rot werden, keine Ahnung haben, ob es sich um einen Fehler oder nur um einen Teil des nicht deterministischen Verhaltens handelt. Normalerweise ist bei diesen Tests ein nicht deterministischer Fehler relativ häufig, sodass Sie am Ende mit den Schultern zucken, wenn diese Tests rot werden. Sobald Sie anfangen, einen Regressionstestfehler zu ignorieren, ist dieser Test nutzlos und Sie können ihn genauso gut wegwerfen.

Uns sollte klar sein, dass nicht-deterministische Tests schädlich sind, und wir sollten sie um jeden Preis vermeiden.

Was ist also die häufigste Ursache für nicht deterministische Datenzugriffstests?

Meine Erfahrung hat mich gelehrt, dass der häufigste Grund für nicht deterministische Datenzugriffstests darin besteht, dass die Datenbank nicht in einen bekannten Zustand initialisiert wird, bevor jeder Testfall ausgeführt wird.

Das ist traurig, weil dies ein wirklich einfach zu lösendes Problem ist. Tatsächlich können wir es lösen, indem wir eine dieser Optionen verwenden:

  1. Wir können der Datenbank Informationen hinzufügen, indem wir die anderen Methoden des getesteten Repositorys verwenden.
  2. Wir können eine Bibliothek schreiben, die unsere Datenbank initialisiert, bevor jeder Test ausgeführt wird.
  3. Wir können vorhandene Bibliotheken wie DbUnit und NoSQLUnit verwenden.

Wir müssen jedoch vorsichtig sein, da nur diese Optionen sinnvoll sind .

Die erste Option ist der schlechteste Weg, um dieses Problem zu lösen. Es überhäuft unsere Testmethoden mit unnötigem Initialisierungscode und macht sie sehr anfällig. Wenn wir zum Beispiel die Methode brechen, die verwendet wird, um Informationen in unserer Datenbank zu speichern, wird jeder Test, der sie verwendet, fehlschlagen.

Die zweite Option ist etwas besser. Warum sollten wir jedoch eine neue Bibliothek erstellen, wenn wir eine vorhandene Bibliothek verwenden könnten, die nachweislich funktioniert?

Wir sollten das Rad nicht neu erfinden. Wir sollten dieses Problem lösen, indem wir den einfachsten und besten Weg verwenden. Wir müssen eine vorhandene Bibliothek verwenden.

Regel 3:Wir müssen das Richtige behaupten

Wenn wir Tests für unseren Datenzugriffscode schreiben, müssen wir möglicherweise Tests schreiben, die

  1. Informationen aus der Datenbank lesen.
  2. Informationen in die Datenbank schreiben.

Was für Behauptungen müssen wir schreiben?

Zuerst , wenn die Schreibtests Informationen aus der Datenbank lesen, müssen wir diese Regeln befolgen:

  • Wenn wir ein Framework oder eine Bibliothek (z. B. Spring Data) verwenden, die die aus der Datenbank gefundenen Informationen Objekten zuordnet, macht es keinen Sinn zu behaupten, dass jeder Eigenschaftswert des zurückgegebenen Objekts korrekt ist. In dieser Situation sollten wir sicherstellen, dass der Wert der Eigenschaft, die das zurückgegebene Objekt identifiziert, korrekt ist. Der Grund dafür ist, dass wir nur Frameworks oder Bibliotheken verwenden sollten, denen wir vertrauen. Wenn wir darauf vertrauen, dass unser Datenzugriffs-Framework oder unsere Bibliothek ihre Aufgabe erfüllt, macht es keinen Sinn, alles zu behaupten.
  • Wenn wir ein Repository implementiert haben, das die in der Datenbank gefundenen Informationen Objekten zuordnet, sollten wir sicherstellen, dass jeder Eigenschaftswert des zurückgegebenen Objekts korrekt ist. Wenn wir dies nicht tun, können wir nicht sicher sein, dass unser Repository korrekt funktioniert.

Zweiter , wenn wir Tests schreiben, die Informationen in die Datenbank schreiben, sollten wir unserer Testmethode keine Zusicherungen hinzufügen.

Wir müssen ein Tool wie DbUnit oder NoSQLUnit verwenden, um sicherzustellen, dass die richtigen Informationen in der Datenbank gespeichert werden. Dieser Ansatz hat zwei Vorteile:

  • Wir können unsere Behauptungen auf der richtigen Ebene schreiben. Mit anderen Worten, wir können überprüfen, ob die Informationen wirklich in der verwendeten Datenbank gespeichert sind.
  • Wir können vermeiden, unsere Testmethoden mit Code zu überladen, der die gespeicherten Informationen aus der verwendeten Datenbank findet und überprüft, ob die richtigen Informationen gefunden werden.

Aber was ist, wenn wir sicherstellen wollen, dass die Methode, die Informationen in der Datenbank speichert, die richtigen Informationen zurückgibt?

Nun, wenn wir diese Methode selbst implementiert haben, müssen wir zwei Tests für diese Methode schreiben:

  1. Wir müssen sicherstellen, dass die richtigen Informationen in der Datenbank gespeichert werden.
  2. Wir müssen überprüfen, ob die Methode die richtigen Informationen zurückgibt.

Wenn uns diese Methode andererseits von einem Framework oder einer Bibliothek zur Verfügung gestellt wird, sollten wir keine Tests dafür schreiben.

Wir dürfen nicht vergessen, dass unser Ziel nicht darin besteht, Behauptungen zu schreiben, die sicherstellen, dass das verwendete Datenzugriffs-Framework oder die Bibliothek korrekt funktioniert.

Unser Ziel ist es, Behauptungen zu schreiben, die sicherstellen, dass unser Code korrekt funktioniert.

Zusammenfassung

Dieser Blogbeitrag hat uns vier Dinge gelehrt:

  • Wenn wir die Vorteile unserer Datenzugriffstests maximieren möchten, müssen unsere Integrationstests dasselbe Datenbankschema verwenden, das verwendet wird, wenn unsere Anwendung in der Produktionsumgebung bereitgestellt wird.
  • Nicht-deterministische Tests loszuwerden ist einfach. Alles, was wir tun müssen, ist, unsere Datenbank in einen bekannten Zustand zu initialisieren, bevor jeder Testfall ausgeführt wird, indem wir eine Bibliothek wie DbUnit oder NoSQLUnit verwenden.
  • Wenn wir überprüfen müssen, ob die richtigen Informationen in der verwendeten Datenbank gespeichert sind, müssen wir eine Bibliothek wie DbUnit oder NoSQLUnit verwenden.
  • Wenn wir überprüfen möchten, ob die richtigen Informationen von der verwendeten Datenbank zurückgegeben werden, müssen wir Zusicherungen schreiben, die sicherstellen, dass unser Code funktioniert.

Java-Tag