Wie wende ich TDD auf Lese- / Schreibfunktionen an?

10

Es scheint ein Henne-Ei-Problem zu sein.

Sie können eine Schreibfunktion in einen Datenspeicher schreiben lassen, wissen jedoch nie, dass Sie sie ohne eine getestete Lesefunktion ordnungsgemäß gespeichert haben.

Sie können eine Lesefunktion aus einem Datenspeicher lesen lassen, aber wie können Sie Inhalte in diesen Datenspeicher einfügen, um sie ohne eine getestete Schreibfunktion zu lesen?

BEARBEITEN:

Ich verbinde mich mit einer SQL-Datenbank und mache Transaktionen mit dieser, um Objekte zur Verwendung zu speichern und zu laden. Es macht keinen Sinn, die von der Datenbank bereitgestellten Zugriffsfunktionen zu testen, aber ich verpacke solche DB-Funktionen, um die Objekte zu serialisieren / deserialisieren. Ich möchte sichergehen, dass ich die richtigen Dinge zur und von der DB richtig schreibe und lese.

Es ist nicht wie Hinzufügen / Löschen, wie @snowman erwähnt. Ich möchte wissen, dass der Inhalt, den ich geschrieben habe, korrekt ist, aber das erfordert eine gut getestete Lesefunktion. Wenn ich lese, möchte ich sichergehen, dass mein Lesen ein Objekt korrekt erstellt hat, das dem entspricht, was geschrieben wurde. Dies erfordert jedoch eine gut getestete Schreibfunktion.

user2738698
quelle
Schreiben Sie Ihren eigenen Datenspeicher oder verwenden Sie einen vorhandenen? Wenn Sie ein vorhandenes verwenden, gehen Sie davon aus, dass es bereits funktioniert. Wenn Sie Ihre eigene Software schreiben, funktioniert dies genauso wie das Schreiben anderer Software mit TDD.
Robert Harvey
1
Obwohl eng verwandt, denke ich nicht, dass dies ein Duplikat dieser speziellen Frage ist. Das betrogene Ziel spricht von Hinzufügen / Löschen, dieses ist Lesen / Schreiben. Der Unterschied ist ein Lese- / Schreibabhängigkeit wahrscheinlich stützt sich auf die Inhalte werden von Objekten gelesen / geschrieben, während ein einfaches Hinzufügen / Löschen Test wahrscheinlich viel einfacher wäre: Hat das Objekt existieren oder nicht.
2
Sie können eine Datenbank mit Daten und überhaupt keiner Schreibfunktion bereitstellen, um eine Lesefunktion zu testen.
JeffO

Antworten:

7

Beginnen Sie mit der Lesefunktion.

  • Im Testaufbau : Erstellen Sie die Datenbank und fügen Sie Testdaten hinzu. entweder über Migrationsskripte oder aus einem Backup. Da dies nicht Ihr Code ist, ist kein Test in TDD erforderlich

  • Im Test : Instanziieren Sie Ihr Repository, zeigen Sie auf Ihre Testdatenbank und rufen Sie die Read-Methode auf. Überprüfen Sie, ob die Testdaten zurückgegeben werden.

Nachdem Sie eine vollständig getestete Lesefunktion haben, können Sie zur Schreibfunktion wechseln, die das vorhandene Lesen verwenden kann, um die eigenen Ergebnisse zu überprüfen

Ewan
quelle
Ich denke, Sie könnten eine Datenbank im Speicher erstellen, um die Dinge zu beschleunigen, aber das könnte zu komplex sein. Warum nicht stattdessen Mocks in Unit-Tests verwenden?
BЈовић
1
Ein bisschen Devil's Advocate hier, aber wie testen Sie, ob die Datenbank korrekt erstellt wurde? Wie OP sagte, Huhn und Ei.
user949300
1
@Ewan - Ich stimme absolut zu, dass Sie ihren DB-Code nicht testen sollten. Aber woher wissen Sie, dass Ihr DB-Setup-Code irgendwo kein INSERT vergessen oder einen falschen Wert in eine Spalte eingefügt hat?
user949300
1
Bei einem reinen TDD-Ansatz ist der Test die Voraussetzung. Es kann also logischerweise nicht falsch sein. obvs in der realen Welt muss man es mustern
Ewan
1
Quis custodiet ipsos custodes? Oder: "Wer testet die Tests?" :-) Ich stimme Ihnen zu, dass dies in einer puristischen TDD-Welt die schrecklich mühsame und fehleranfällige Methode wäre (insbesondere wenn es sich um eine komplizierte Struktur mit mehreren Tabellen mit 8 JOINS handelt). Upvoted.
user949300
6

Ich schreibe oft nur, gefolgt von einem Lesen. zB (Pseudocode)

Foo foo1 = setup some object to write
File tempfile = create a tempfile, possibly in memory 
writeFoo(foo1, tempfile) 
Foo foo2 = readFoo(tempfile) 
assertEquals(foo1, foo2); 
clean-up goes here

Später hinzugefügt

Zusätzlich dazu, dass diese Lösung "prgamatisch" und "gut genug" ist, könnte man argumentieren, dass die anderen Lösungen das Falsche testen . Zu testen, ob die Zeichenfolgen oder SQL-Anweisungen übereinstimmen, ist keine schreckliche Idee. Ich habe es selbst gemacht, aber es testet einen Nebeneffekt und ist fragil. Was passiert, wenn Sie die Groß- und Kleinschreibung ändern, ein Feld hinzufügen oder eine Versionsnummer in Ihren Daten aktualisieren? Was passiert, wenn Ihr SQL-Treiber die Reihenfolge der Aufrufe aus Effizienzgründen ändert oder Ihr aktualisierter XML-Serializer zusätzlichen Speicherplatz hinzufügt oder eine Schemaversion ändert?

Wenn Sie sich nun strikt an eine offizielle Spezifikation halten müssen, stimme ich zu, dass die Überprüfung der Details angemessen ist.

user949300
quelle
1
Weil es zu 90% wirklich dichter Pseudocode ist? Nicht sicher. Vielleicht den Text hervorheben und den Code leiser machen?
RubberDuck
1
Ja @Ewan. Der Eiferer würde dies missbilligen, aber der pragmatische Programmierer würde "gut genug" sagen und weitermachen.
RubberDuck
1
Ich las die Frage irgendwie als ... "Angenommen, ich folge TDD wie ein Eiferer ..."
Ewan
1
Meine Interpretation von OP und TDD ist, dass Ihr Test zuerst geschrieben werden sollte und nicht sowohl Lesen als auch Schreiben verwenden sollte, es sei denn, einer wird auch an anderer Stelle getestet.
Ewan
2
Sie testen, "Lesen sollte zurückgeben, was ich schreibe", aber die Anforderungen lauten "Lesen sollte Daten aus der Datenbank zurückgeben" und "Schreiben sollte Daten in die Datenbank schreiben"
Ewan
4

Tu es nicht. Testen Sie keine E / A. Das ist Zeitverschwendung.

Unit-Test-Logik. Wenn Sie eine Menge Logik im E / A-Code testen möchten, sollten Sie Ihren Code umgestalten, um die Logik der E / A-Ausführung und der E / A-Ausführung von der eigentlichen E / A-Ausführung zu trennen (was fast unmöglich zu testen ist).

Wenn Sie einen HTTP-Server testen möchten, sollten Sie zwei Arten von Tests durchführen: Integrationstests und Komponententests. Die Komponententests sollten überhaupt nicht mit E / A interagieren. Das ist langsam und führt zu vielen Fehlerbedingungen, die nichts mit der Richtigkeit Ihres Codes zu tun haben. Unit-Tests sollten nicht dem Status Ihres Netzwerks unterliegen!

Ihr Code sollte sich trennen:

  • Die Logik zum Bestimmen, welche Informationen gesendet werden sollen
  • Die Logik zum Bestimmen, welche Bytes gesendet werden sollen, um ein bestimmtes Informationsbit zu senden (wie codiere ich eine Antwort usw. in Rohbytes) und
  • Der Mechanismus zum tatsächlichen Schreiben dieser Bytes in einen Socket.

Die ersten beiden beinhalten Logik und Entscheidungen und erfordern Unit-Tests. Letzteres beinhaltet nicht viele oder gar keine Entscheidungen und kann mithilfe von Integrationstests wunderbar getestet werden.

Dies ist im Allgemeinen nur gutes Design, aber einer der Gründe dafür ist, dass es einfacher zu testen ist.


Hier sind einige Beispiele:

  • Wenn Sie Code schreiben, der Daten aus einer relationalen Datenbank abruft, können Sie in einem Unit-Test testen, wie Sie die von relationalen Abfragen zurückgegebenen Daten Ihrem Anwendungsmodell zuordnen.
  • Wenn Sie Code schreiben, der Daten in eine relationale Datenbank schreibt, können Sie in einem Unit-Test testen, welche Daten in die Datenbank geschrieben werden sollen, ohne die von Ihnen verwendeten SQL-Abfragen tatsächlich zu testen. Beispielsweise können Sie zwei Kopien Ihres Anwendungsstatus im Speicher behalten: eine Kopie, die darstellt, wie die Datenbank aussieht, und die Arbeitskopie. Wenn Sie mit der Datenbank synchronisieren möchten, müssen Sie diese unterscheiden und die Unterschiede in die Datenbank schreiben. Sie können diesen Diff-Code ganz einfach einem Unit-Test unterziehen.
  • Wenn Sie Code schreiben, der etwas aus einer Konfigurationsdatei liest, möchten Sie den Parser für das Konfigurationsdateiformat testen, jedoch mit Zeichenfolgen aus Ihrer Testquelldatei und nicht mit Zeichenfolgen, die Sie von der Festplatte erhalten.
Miles Rout
quelle
2

Ich weiß nicht, ob dies eine Standardpraxis ist oder nicht, aber es funktioniert gut für mich.

In meinem nicht-Datenbank Lese - Schreib - Methode Implementierungen verwende ich meine eigenen typspezifischen toString()und fromString()Methoden als Implementierungsdetails.

Diese können leicht isoliert getestet werden:

 assertEquals("<xml><car type='porsche'>....", new Car("porsche").toString());

Für die tatsächlichen Lese-Schreib-Methoden habe ich einen Integrationstest, der in einem Test physisch liest und schreibt

Übrigens: Stimmt etwas nicht, wenn ein Test zusammen liest / schreibt?

k3b
quelle
Es mag sich nicht gut oder "rein" anfühlen, aber dies ist die pragmatische Lösung.
RubberDuck
Ich mag auch die Idee, Lesen und Schreiben zusammen zu testen. Ihr toString () ist ein schöner pragmatischer Kompromiss.
user949300
1

Bekannte Daten müssen auf bekannte Weise formatiert werden. Der einfachste Weg, dies zu implementieren, besteht darin, eine konstante Zeichenfolge zu verwenden und das Ergebnis zu vergleichen, wie in @ k3b beschrieben.

Sie sind jedoch nicht auf Konstanten beschränkt. Es kann eine Reihe von Eigenschaften der geschriebenen Daten geben, die Sie mit einem anderen Parser extrahieren können, z. B. reguläre Ausdrücke oder sogar Ad-hoc-Tests, die nach Merkmalen der Daten suchen.

Zum Lesen oder Schreiben der Daten kann es nützlich sein, über ein In-Memory-Dateisystem zu verfügen, mit dem Sie Ihre Tests ausführen können, ohne dass andere Teile des Systems eingreifen können. Wenn Sie keinen Zugriff auf ein gutes In-Memory-Dateisystem haben, verwenden Sie einen temporären Verzeichnisbaum.

BobDalgleish
quelle
1

Verwenden Sie Abhängigkeitsinjektion und Verspottung.

Sie möchten Ihren SQL-Treiber nicht testen und Sie möchten nicht testen, ob Ihre SQL-Datenbank online und ordnungsgemäß eingerichtet ist. Das wäre Teil eines Integrations- oder Systemtests. Sie möchten testen, ob Ihr Code die SQL-Anweisungen sendet, die er senden soll, und ob er die Antworten so interpretiert, wie er soll.

Wenn Sie also eine Methode / Klasse haben, die etwas mit einer Datenbank tun soll, muss diese Datenbankverbindung nicht von selbst hergestellt werden. Ändern Sie es so, dass das Objekt, das die Datenbankverbindung darstellt, an es übergeben wird.

Übergeben Sie in Ihrem Produktionscode das eigentliche Datenbankobjekt.

Übergeben Sie in Ihren Komponententests ein Scheinobjekt, das sich so verhält, als würde eine tatsächliche Datenbank keinen Datenbankserver kontaktieren. Lassen Sie es einfach überprüfen, ob es die SQL-Anweisungen empfängt, die es empfangen soll, und antwortet dann mit fest codierten Antworten.

Auf diese Weise können Sie Ihre Datenbankabstraktionsschicht testen, ohne eine tatsächliche Datenbank zu benötigen.

Philipp
quelle
Devil's Advocate: Woher wissen Sie, welche SQL-Anweisungen es "empfangen" soll? Was ist, wenn der DB-Treiber die Reihenfolge anhand der Angaben im Code optimiert?
user949300
@ user949300 Ein Datenbank-Mock-Objekt ersetzt normalerweise den Datenbanktreiber.
Philipp
Wenn Sie ein Repository testen, macht es keinen Sinn, einen verspotteten Datenbankclient zu injizieren. Sie müssen testen, ob auf Ihrem Code SQL ausgeführt wird, das in der Datenbank funktioniert. Andernfalls testen Sie am Ende nur Ihren Mock
Ewan
@Ewan Darum geht es beim Unit-Test nicht. Ein Komponententest testet eine Codeeinheit, die vom Rest der Welt isoliert ist. Sie testen nicht die Interaktionen zwischen Komponenten wie Ihrem Code und der Datenbank. Dafür sind Integrationstests gedacht.
Philipp
Ja. Ich sage, es gibt keine sinnvolle Einheit, die ein Datenbank-Repository testet. Integrationstest ist das einzige, was es wert ist
Ewan
0

Wenn Sie einen objektrelationalen Mapper verwenden, gibt es normalerweise eine zugeordnete Bibliothek, mit der Sie testen können, ob Ihre Zuordnungen ordnungsgemäß funktionieren, indem Sie ein Aggregat erstellen, es beibehalten und aus einer neuen Sitzung neu laden und anschließend den Status überprüfen das ursprüngliche Objekt.

NHibernate bietet Persistenzspezifikationstests an . Es kann so konfiguriert werden, dass es für schnelle Komponententests gegen einen In-Memory-Speicher arbeitet.

Wenn Sie der einfachsten Version der Muster für Repository und Arbeitseinheit folgen und alle Ihre Zuordnungen testen, können Sie sich darauf verlassen, dass die Dinge ziemlich gut funktionieren.

pnschofield
quelle