In der Softwareentwicklung nimmt das Testen von Anwendungen und Beheben von Fehlern einen großen Teil der Arbeitszeit ein. Sind die Module einer Software durch lose Kopplung der Abhängigkeiten klar getrennt, lässt sich die Software unter Verwendung von Dependency Injection und Mock-Objekten isoliert testen. Werden jedoch externe Ressourcen von der Software verwendet, wie z.B. USB-Geräte oder Datenbanken, gestaltet sich das automatisierte Testen schwieriger. Deshalb geht es in diesem Blogbeitrag um das Testen einer Datenbankanbindung mit dem Entity Framework 6.
Die Abb. 1 zeigt die grobe Softwarearchitektur einer einfachen ASP.NET MVC App. Die App baut sich aus mehreren Schichten mit ihren jeweils definierten Aufgabenbereichen auf. Jede der vier Schichten kennt nur die darüberliegende und kann auch nur auf die Funktionalitäten dieser Schicht zugreifen (symbolisiert über die Richtung der Pfeile).
Auf der “Controller Layer” -Schicht gehen die Webanfragen ein, es werden Validierungen der Eingangsdaten durchgeführt, Benutzer autorisiert sowie Ausnahmen der Service Layer abgefangen und Fehlernachrichten für den User generiert.
Die “Service Layer”-Schicht ist für die eigentliche App-Logik verantwortlich und verwendet die “Repository Layer”-Schicht zum Abfragen, Speichern und Bearbeiten von Datensätzen.
Die Repository Schicht kapselt den Zugriff auf eine Datenbank und abstrahiert damit die Art der Datenbank (SQL- oder dokumentenbasierte Datenbank) sowie den Zugriff auf die Datenbank (z.B. über einen ORM wie EF6 oder NHibernate).
Abb. 1: ASP.NET App Softwarearchitektur
In diesem Beitrag geht es um das Testen der Repository Layer, die auf eine SQL-Datenbank über das Entity Framework 6 zugreift. Da wir auch komplexe Abfragen über das Entity Framework realisieren möchten, ist es hilfreich, diese automatisiert testen zu können. Auf die zahlreichen Vorteile von Unit-Tests wird bereits ausführlich in einem anderen Blogbeitrag eingegangen, weshalb es hier nicht nochmals wiederholt wird. Genau genommen handelt es sich bei unseren Tests der Repository Layer nicht um Unit-Tests, sondern um Integration Tests, da wir auf eine Datenbank zugreifen und somit mehr als eine Schicht unserer Software gleichzeitig testen.
Die Abb. 2 zeigt einen sehr einfachen Test, aufgeteilt auf die drei Bereiche Arrange, Act und Assert. Als Testframework wird hier MSTest verwendet, was sich an den Attributdekorationen der Klasse [TestClass] und der Methode [TestMethod] erkennen lässt. Im ersten Bereich Arrange werden alle benötigten Vorbedingungen für den Test generiert – das Anlegen und Speichern eines neuen ApplicationUsers (Zeile 28 und 29).
Hinweis: Um die Logik des Tests transparent darzustellen wird hier auf die Repository Schicht verzichtet und direkt auf den DbContext des Entity Frameworks zugegriffen. Bei Verwendung der Repository Layer würde der DbContext als Abhängigkeit in diese Schicht injiziert werden.
Der zu testende Code befindet sich in den Zeilen 32 und 40. Es wird versucht, ein neues Dokument hinzuzufügen und zu speichern. Das neue Dokument entspricht jedoch nicht den Regeln der Datenbank, da die erforderlichen Eigenschaften FilePath und Content nicht gesetzt sind. Deshalb erwarten wir, dass eine Ausnahme beim Speichern der Änderungen angezeigt wird. Wir testen hier also das Schema der Datenbank und die gewünschte Reaktion auf das Einfügen fehlerhafter Daten.
Hinweis: Für die Asserts wird hier das NuGet Package FluentAssertions verwendet.
Abb. 2: Ein simpler Test
Der Test in Abb. 2 ist lauffähig und das Häkchen zwischen den Zeilen 18 und 19 symbolisiert, dass der Test erfolgreich durchgeführt wurde. Doch leider läuft der Test in diesem Fall nur einmalig erfolgreich durch. Ein weiterer Testlauf führt zu einem fehlerhaften Ergebnis, da in der Zeile 29 eine Ausnahme von der Datenbank generiert wird. Dies geschieht, weil der “TestUser” bereits durch den vorherigen Testdurchlauf der Datenbank hinzugefügt wurde und der UserName mit einem eindeutigen Index versehen ist (siehe Abb. 3).
Abb. 3: Tabelle der Datenbank im SQL Server Management Studio
Durch das Ausführen unserer Tests wird die Datenbank manipuliert, so dass sie sich beim Start eines neuen Testlaufs nicht mehr in einem definierten Zustand befindet. Die Datenbank sollte also zu Beginn jedes Tests vollständig leer sein.
Damit mehrere konkurrierende Tests simultan laufen können, sollten zu keiner Zeit Datensätze in der Datenbank persistent gespeichert werden. Dieses gewünschte Verhalten lässt sich erreichen, indem alle Datenbankoperationen eines Tests in einer Transaktion laufen. Am Ende des Tests wird ein Rollback der Transaktion durchgeführt, so dass keine der Änderungen außerhalb der Transaktion sichtbar werden. Um die Komplexität der einzelnen Tests so gering wie möglich zu halten, erstellen wir dafür eine einfache Basisklasse (Abb. 4) und lassen unsere Testklassen von dieser erben.
Abb. 4: Basisklasse für Tests mit dem Entity Framework
Durch das Erben von der Basisklasse wird vor jedem einzelnen Testlauf ein TransactionScope Objekt erzeugt, damit alle Operationen über das Entity Framework in einer Transaktion laufen, die nie persistiert wird. Außerdem werden zwei DbContext Objekte erstellt, die zum Testen und zur Generierung des benötigten Testszenarios vorgesehen sind. Selbstverständlich können auch weitere DbContext Instanzen in den Tests verwendet werden.
Damit sich der DbContext in unserem Testprojekt überhaupt mit einer Datenbank verbinden kann, muss noch der Connection String in der App.config Datei (Abb. 5) des Testprojekts konfiguriert werden. Für die automatisierten Tests ist die LocalDB völlig ausreichend. Wenn auf dem Testsystem die Datenbank noch nicht verfügbar ist, muss das Testprojekt noch sicherstellen, dass die Datenbank erstellt und auf das aktuelle Schema migriert wird. Dieses wäre natürlich als manueller Schritt über die PMC (Package Manager Console) möglich, hätte aber zur Folge, dass die Tests beim Continuous Integration auf einem Agent nicht erfolgreich durchlaufen.
Abb. 5: Connection String für LocalDB
Damit die lokale Testdatenbank schematisch immer dem Stand des Entity Frameworks entspricht, wird noch eine weitere Klasse eingeführt (siehe Abb. 6). Die Klasse enthält nur eine Methode, die vor jedem Lauf des Test-Runners einmalig (Methode mit AssemblyInitialize dekoriert) ausgeführt wird. In dieser Methode werden unsere Migrationen vom Entity Framework auf die Datenbank angewendet.
Abb. 6: Erstellen und Migrieren der Datenbank
Unsere Tests laufen gegen eine leere lokale Datenbank. So können wir das Verhalten unserer Software, basierend auf unserem Datenbankschema, über das Entity Framework automatisiert testen. Die Tests sind auch in Continous Integration Lösungen lauffähig und können beispielsweise mit dem “Hosted VS2017” Agent des Visual Studio Team Services erfolgreich ausgeführt werden.
Durch das automatisierte Testen von Applikationen wird viel Zeit eingespart, da sich der Aufwand für manuelle Testprozeduren reduziert. Außerdem lassen sich Fehler, die z.B. beim Einpflegen neuer Änderungen entstehen, frühzeitig erkennen.
Titelbildnachweis: https://pixabay.com/de/chemie-lehrer-wissenschaft-labor-1027781/
Was denkst du?