So testen Sie ein Objekt mit Datenbankabfragen


152

Ich habe gehört, dass Unit-Tests "total fantastisch", "wirklich cool" und "alle möglichen guten Dinge" sind, aber 70% oder mehr meiner Dateien beinhalten Datenbankzugriff (einige lesen und andere schreiben) und ich bin mir nicht sicher, wie um einen Unit-Test für diese Dateien zu schreiben.

Ich verwende PHP und Python, aber ich denke, es ist eine Frage, die für die meisten / alle Sprachen gilt, die Datenbankzugriff verwenden.

Antworten:


82

Ich würde vorschlagen, Ihre Anrufe in die Datenbank zu verspotten. Mocks sind im Grunde genommen Objekte, die wie das Objekt aussehen, für das Sie eine Methode aufrufen möchten, in dem Sinne, dass sie dieselben Eigenschaften, Methoden usw. haben, die dem Aufrufer zur Verfügung stehen. Aber anstatt die Aktion auszuführen, für die sie programmiert sind, wenn eine bestimmte Methode aufgerufen wird, überspringt sie diese insgesamt und gibt nur ein Ergebnis zurück. Dieses Ergebnis wird normalerweise von Ihnen im Voraus definiert.

Um Ihre Objekte für das Verspotten einzurichten, müssen Sie wahrscheinlich eine Art Inversion des Kontroll- / Abhängigkeitsinjektionsmusters verwenden, wie im folgenden Pseudocode:

class Bar
{
    private FooDataProvider _dataProvider;

    public instantiate(FooDataProvider dataProvider) {
        _dataProvider = dataProvider;
    }

    public getAllFoos() {
        // instead of calling Foo.GetAll() here, we are introducing an extra layer of abstraction
        return _dataProvider.GetAllFoos();
    }
}

class FooDataProvider
{
    public Foo[] GetAllFoos() {
        return Foo.GetAll();
    }
}

Jetzt erstellen Sie in Ihrem Komponententest ein Modell von FooDataProvider, mit dem Sie die Methode GetAllFoos aufrufen können, ohne die Datenbank tatsächlich aufrufen zu müssen.

class BarTests
{
    public TestGetAllFoos() {
        // here we set up our mock FooDataProvider
        mockRepository = MockingFramework.new()
        mockFooDataProvider = mockRepository.CreateMockOfType(FooDataProvider);

        // create a new array of Foo objects
        testFooArray = new Foo[] {Foo.new(), Foo.new(), Foo.new()}

        // the next statement will cause testFooArray to be returned every time we call FooDAtaProvider.GetAllFoos,
        // instead of calling to the database and returning whatever is in there
        // ExpectCallTo and Returns are methods provided by our imaginary mocking framework
        ExpectCallTo(mockFooDataProvider.GetAllFoos).Returns(testFooArray)

        // now begins our actual unit test
        testBar = new Bar(mockFooDataProvider)
        baz = testBar.GetAllFoos()

        // baz should now equal the testFooArray object we created earlier
        Assert.AreEqual(3, baz.length)
    }
}

Kurz gesagt, ein häufiges Spott-Szenario. Natürlich möchten Sie wahrscheinlich auch weiterhin Ihre tatsächlichen Datenbankaufrufe testen, für die Sie die Datenbank aufrufen müssen.


Ich weiß, dass dies alt ist, aber was ist mit dem Erstellen einer doppelten Tabelle zu der, die sich bereits in der Datenbank befindet. Auf diese Weise können Sie bestätigen, dass DB-Aufrufe funktionieren?
bretterer

1
Ich habe das PDO von PHP als meinen niedrigsten Zugriff auf die Datenbank verwendet, über die ich eine Schnittstelle extrahiert habe. Dann habe ich darüber hinaus eine anwendungsorientierte Datenbankebene erstellt. Dies ist die Ebene, die alle unformatierten SQL-Abfragen und andere Informationen enthält. Der Rest der Anwendung interagiert mit dieser übergeordneten Datenbank. Ich habe festgestellt, dass dies für Unit-Tests ziemlich gut funktioniert. Ich teste meine Anwendungsseiten dahingehend, wie sie mit der Anwendungsdatenbank interagieren. Ich teste meine Anwendungsdatenbank auf die Interaktion mit PDO. Ich gehe davon aus, dass PDO ohne Fehler funktioniert. Quellcode: manx.codeplex.com
legalisieren

1
@bretterer - Das Erstellen einer doppelten Tabelle eignet sich für Integrationstests. Für Komponententests verwenden Sie normalerweise ein Scheinobjekt, mit dem Sie eine Codeeinheit unabhängig von der Datenbank testen können.
BornToCode

2
Welchen Wert hat das Verspotten von Datenbankaufrufen in Ihren Komponententests? Dies scheint nicht sinnvoll zu sein, da Sie die Implementierung ändern könnten, um ein anderes Ergebnis zurückzugeben, Ihr Komponententest jedoch (fälschlicherweise) bestanden würde.
Mai 2

2
@ bmay2 Du liegst nicht falsch. Meine ursprüngliche Antwort wurde vor langer Zeit (9 Jahre!) Geschrieben, als viele Leute ihren Code nicht auf testbare Weise schrieben und wenn Testwerkzeuge stark fehlten. Ich würde diesen Ansatz nicht mehr empfehlen. Heute würde ich einfach eine Testdatenbank einrichten und sie mit den Daten füllen, die ich für den Test benötigte, und / oder meinen Code entwerfen, damit ich so viel Logik wie möglich ohne Datenbank testen kann.
Doug R

25

Im Idealfall sollten Ihre Objekte dauerhaft unwissend sein. Zum Beispiel sollten Sie eine "Datenzugriffsschicht" haben, an die Sie Anforderungen stellen würden, die Objekte zurückgeben würde. Auf diese Weise können Sie diesen Teil aus Ihren Komponententests herauslassen oder isoliert testen.

Wenn Ihre Objekte eng mit Ihrer Datenschicht verbunden sind, ist es schwierig, ordnungsgemäße Unit-Tests durchzuführen. Der erste Teil des Komponententests ist "Einheit". Alle Geräte sollten isoliert getestet werden können.

In meinen c # -Projekten verwende ich NHibernate mit einer vollständig separaten Datenschicht. Meine Objekte befinden sich im Kerndomänenmodell und werden von meiner Anwendungsschicht aus aufgerufen. Die Anwendungsschicht spricht sowohl mit der Datenschicht als auch mit der Domänenmodellschicht.

Die Anwendungsschicht wird manchmal auch als "Geschäftsschicht" bezeichnet.

Wenn Sie PHP verwenden, erstellen Sie einen bestimmten Satz von Klassen für NUR den Datenzugriff. Stellen Sie sicher, dass Ihre Objekte keine Ahnung haben, wie sie beibehalten werden, und verbinden Sie die beiden in Ihren Anwendungsklassen.

Eine andere Möglichkeit wäre die Verwendung von Mocking / Stubs.


Ich habe dem immer zugestimmt, aber in der Praxis ist dies aufgrund von Fristen und "Okay, jetzt nur noch eine Funktion hinzufügen, bis heute 14 Uhr" eines der schwierigsten Dinge, die es zu erreichen gilt. Diese Art von Dingen ist jedoch ein Hauptziel des Refactorings, falls mein Chef jemals entscheidet, dass er nicht an 50 neue Probleme gedacht hat, die eine völlig neue Geschäftslogik und Tabellen erfordern.
Darren Ringer

3
Wenn Ihre Objekte eng mit Ihrer Datenschicht verbunden sind, ist es schwierig, ordnungsgemäße Unit-Tests durchzuführen. Der erste Teil des Komponententests ist "Einheit". Alle Geräte sollten isoliert getestet werden können. nette
Erklärung

11

Der einfachste Weg, ein Objekt mit Datenbankzugriff einem Komponententest zu unterziehen, ist die Verwendung von Transaktionsbereichen.

Beispielsweise:

    [Test]
    [ExpectedException(typeof(NotFoundException))]
    public void DeleteAttendee() {

        using(TransactionScope scope = new TransactionScope()) {
            Attendee anAttendee = Attendee.Get(3);
            anAttendee.Delete();
            anAttendee.Save();

            //Try reloading. Instance should have been deleted.
            Attendee deletedAttendee = Attendee.Get(3);
        }
    }

Dadurch wird der Status der Datenbank zurückgesetzt, im Grunde wie bei einem Transaktions-Rollback, sodass Sie den Test so oft ausführen können, wie Sie möchten, ohne dass Nebenwirkungen auftreten. Wir haben diesen Ansatz erfolgreich in großen Projekten eingesetzt. Unser Build dauert zwar etwas lange (15 Minuten), aber es ist nicht schrecklich, 1800 Unit-Tests durchzuführen. Wenn die Build-Zeit ein Problem darstellt, können Sie den Build-Prozess so ändern, dass mehrere Builds vorhanden sind, einer zum Erstellen von src und einer, der anschließend gestartet wird und Unit-Tests, Code-Analyse, Verpackung usw. übernimmt.


1
+1 Spart viel Zeit beim Testen von Datenzugriffsebenen. Beachten Sie nur, dass TS häufig MSDTC benötigt, was möglicherweise nicht wünschenswert ist (abhängig davon, ob Ihre App MSDTC benötigt)
StuartLC

Die ursprüngliche Frage betraf PHP. Dieses Beispiel scheint C # zu sein. Die Umgebungen sind sehr unterschiedlich.
legalisieren

2
Der Autor der Frage gab an, dass es sich um eine allgemeine Frage handelt, die für alle Sprachen gilt, die etwas mit einer Datenbank zu tun haben.
Vedran

9
und diese lieben Freunde heißen Integrationstests
AA.

10

Ich kann Ihnen vielleicht einen Vorgeschmack auf unsere Erfahrungen geben, als wir uns mit Unit-Tests für unseren Middle-Tier-Prozess befassten, der eine Menge SQL-Operationen mit "Geschäftslogik" umfasste.

Wir haben zuerst eine Abstraktionsschicht erstellt, mit der wir jede vernünftige Datenbankverbindung "einstecken" konnten (in unserem Fall haben wir einfach eine einzelne ODBC-Verbindung unterstützt).

Sobald dies geschehen war, konnten wir so etwas in unserem Code tun (wir arbeiten in C ++, aber ich bin sicher, Sie haben die Idee):

GetDatabase (). ExecuteSQL ("INSERT INTO foo (bla, bla)")

Zur normalen Laufzeit würde GetDatabase () ein Objekt, das alle unsere SQL-Daten (einschließlich Abfragen) über ODBC eingespeist hat, direkt an die Datenbank zurückgeben.

Wir haben uns dann mit In-Memory-Datenbanken befasst - das mit Abstand beste scheint SQLite zu sein. ( http://www.sqlite.org/index.html ). Es ist bemerkenswert einfach einzurichten und zu verwenden und ermöglicht es uns, GetDatabase () in Unterklassen zu überschreiben und zu überschreiben, um SQL an eine speicherinterne Datenbank weiterzuleiten, die für jeden durchgeführten Test erstellt und zerstört wurde.

Wir befinden uns noch im Anfangsstadium, aber es sieht bisher gut aus. Wir müssen jedoch sicherstellen, dass wir alle erforderlichen Tabellen erstellen und sie mit Testdaten füllen. Wir haben jedoch die Arbeitslast hier durch Erstellen etwas reduziert Ein allgemeiner Satz von Hilfsfunktionen, die all dies für uns erledigen können.

Insgesamt hat dies bei unserem TDD-Prozess immens geholfen, da das Vornehmen von scheinbar harmlosen Änderungen zur Behebung bestimmter Fehler seltsame Auswirkungen auf andere (schwer zu erkennende) Bereiche Ihres Systems haben kann - aufgrund der Natur von SQL / Datenbanken.

Natürlich haben sich unsere Erfahrungen auf eine C ++ - Entwicklungsumgebung konzentriert, aber ich bin sicher, dass Sie unter PHP / Python vielleicht etwas Ähnliches zum Laufen bringen könnten.

Hoffe das hilft.


9

Sie sollten den Datenbankzugriff verspotten, wenn Sie Ihre Klassen einem Komponententest unterziehen möchten. Schließlich möchten Sie die Datenbank nicht in einem Komponententest testen. Das wäre ein Integrationstest.

Abstrakt die Anrufe weg und füge dann einen Mock ein, der nur die erwarteten Daten zurückgibt. Wenn Ihre Klassen nicht mehr als nur Abfragen ausführen, lohnt es sich möglicherweise nicht einmal, sie zu testen ...


6

Das Buch xUnit Test Patterns beschreibt einige Möglichkeiten, um mit Unit-Testing-Code umzugehen, der auf eine Datenbank trifft. Ich stimme den anderen Leuten zu, die sagen, dass Sie dies nicht tun wollen, weil es langsam ist, aber Sie müssen es irgendwann tun, IMO. Es ist eine gute Idee, die Datenbankverbindung zu verspotten, um übergeordnete Inhalte zu testen. In diesem Buch finden Sie jedoch Vorschläge zu Maßnahmen, die Sie zur Interaktion mit der eigentlichen Datenbank ergreifen können.


4

Optionen, die Sie haben:

  • Schreiben Sie ein Skript, das die Datenbank löscht, bevor Sie Komponententests starten. Füllen Sie dann db mit vordefinierten Daten und führen Sie die Tests aus. Sie können dies auch vor jedem Test tun - es ist langsam, aber weniger fehleranfällig.
  • Injizieren Sie die Datenbank. (Beispiel in Pseudo-Java, gilt aber für alle OO-Sprachen)

    Klassendatenbank {
     public Ergebnisabfrage (String-Abfrage) {... echte Datenbank hier ...}
    }}

    Klasse MockDatabase erweitert Datenbank { public Ergebnisabfrage (String-Abfrage) { return "Scheinergebnis"; }} }}

    Klasse ObjectThatUsesDB { public ObjectThatUsesDB (Datenbank db) { this.database = db; }} }}

    Jetzt verwenden Sie in der Produktion die normale Datenbank und fügen für alle Tests nur die Scheindatenbank ein, die Sie ad hoc erstellen können.

  • Verwenden Sie DB während des größten Teils des Codes überhaupt nicht (das ist sowieso eine schlechte Praxis). Erstellen Sie ein "Datenbank" -Objekt, das anstelle von Ergebnissen normale Objekte zurückgibt (dh Useranstelle eines Tupels zurückgibt {name: "marcin", password: "blah"}). Schreiben Sie alle Ihre Tests mit ad hoc erstellten realen Objekten und schreiben Sie einen großen Test, der von einer Datenbank abhängt, die diese Konvertierung sicherstellt funktioniert OK.

Natürlich schließen sich diese Ansätze nicht gegenseitig aus, und Sie können sie nach Bedarf mischen und anpassen.


3

Das Unit-Testen Ihres Datenbankzugriffs ist einfach genug, wenn Ihr Projekt durchgehend einen hohen Zusammenhalt und eine lose Kopplung aufweist. Auf diese Weise können Sie nur die Dinge testen, die jede bestimmte Klasse tut, ohne alles auf einmal testen zu müssen.

Wenn Sie beispielsweise Ihre Benutzeroberflächenklasse einem Komponententest unterziehen, sollten die von Ihnen geschriebenen Tests nur versuchen, zu überprüfen, ob die Logik in der Benutzeroberfläche wie erwartet funktioniert hat, nicht die Geschäftslogik oder Datenbankaktion, die hinter dieser Funktion steht.

Wenn Sie den tatsächlichen Datenbankzugriff einem Komponententest unterziehen möchten, erhalten Sie tatsächlich eher einen Integrationstest, da Sie vom Netzwerkstapel und Ihrem Datenbankserver abhängig sind. Sie können jedoch überprüfen, ob Ihr SQL-Code die Anforderungen erfüllt machen.

Die verborgene Kraft von Unit-Tests für mich persönlich war, dass ich gezwungen bin, meine Anwendungen viel besser zu gestalten, als ich es ohne sie tun könnte. Dies liegt daran, dass es mir wirklich geholfen hat, mich von der Mentalität "Diese Funktion sollte alles tun" zu lösen.

Leider habe ich keine spezifischen Codebeispiele für PHP / Python, aber wenn Sie ein .NET-Beispiel sehen möchten, habe ich einen Beitrag , der eine Technik beschreibt, mit der ich genau diese Tests durchgeführt habe.


2

Normalerweise versuche ich, meine Tests zwischen dem Testen der Objekte (und gegebenenfalls von ORM) und dem Testen der Datenbank aufzuteilen. Ich teste die Objektseite der Dinge, indem ich die Datenzugriffsaufrufe verspotte, während ich die Datenbankseite der Dinge teste, indem ich die Objektinteraktionen mit der Datenbank teste, die meiner Erfahrung nach normalerweise ziemlich begrenzt sind.

Früher war ich frustriert, Unit-Tests zu schreiben, bis ich anfing, den Datenzugriffsteil zu verspotten, damit ich keine Testdatenbank erstellen oder Testdaten im laufenden Betrieb generieren musste. Durch Verspotten der Daten können Sie alles zur Laufzeit generieren und sicherstellen, dass Ihre Objekte mit bekannten Eingaben ordnungsgemäß funktionieren.


2

Ich habe dies noch nie in PHP getan und Python noch nie verwendet, aber Sie möchten die Aufrufe der Datenbank verspotten. Zu diesem Zweck können Sie ein IoC implementieren, unabhängig davon, ob es sich um ein Tool eines Drittanbieters handelt, oder Sie verwalten es selbst. Anschließend können Sie eine Scheinversion des Datenbankaufrufers implementieren, mit der Sie das Ergebnis dieses gefälschten Aufrufs steuern können.

Eine einfache Form der IoC kann nur durch Codierung in Schnittstellen ausgeführt werden. Dies erfordert eine Art Objektorientierung in Ihrem Code, sodass sie möglicherweise nicht für Ihre Arbeit gilt (ich sage, da ich nur PHP und Python erwähnen muss).

Ich hoffe, das ist hilfreich, wenn Sie sonst nichts zu suchen haben.


2

Ich bin damit einverstanden, dass der erste Zugriff nach der Datenbank auf eine DAO-Schicht reduziert werden sollte, die eine Schnittstelle implementiert. Anschließend können Sie Ihre Logik anhand einer Stub-Implementierung der DAO-Schicht testen.


2

Sie können Mocking-Frameworks verwenden , um das Datenbankmodul zu abstrahieren. Ich weiß nicht, ob PHP / Python welche hat, aber für getippte Sprachen (C #, Java usw.) gibt es viele Möglichkeiten

Dies hängt auch davon ab, wie Sie diesen Datenbankzugriffscode entworfen haben, da einige Designs einfacher zu testen sind als andere, wie in den früheren Beiträgen erwähnt.


2

Das Einrichten von Testdaten für Unit-Tests kann eine Herausforderung sein.

Wenn Sie in Java Spring-APIs zum Testen von Einheiten verwenden, können Sie die Transaktionen auf Einheitenebene steuern. Mit anderen Worten, Sie können Komponententests ausführen, bei denen die Datenbank aktualisiert / eingefügt / gelöscht und die Änderungen rückgängig gemacht werden. Am Ende der Ausführung belassen Sie alles in der Datenbank, wie es war, bevor Sie mit der Ausführung begonnen haben. Für mich ist es so gut wie es nur geht.

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.