Nachdem im ersten Teil BDD mit Cucumber hervorgehoben wurde wenden wir uns nun dem schreiben von Tests zu. Testbarer Code ist eine Kunst die mit Erfahrung reift. Dennoch gibt es Hilfsmittel, die das Schreiben von testbaren Code erleichtern. Heute schauen wir uns zwei Konzepte und die dazu gehörigen Frameworks an. Angenommen wir arbeiten an unserer Online-Videothek und haben gerade das Verhalten zum Ausleihen eines Filmes spezifiziert. Es existiert bereits ein Video-Controller (Videos), der die Funktion boolean rent(User user, Video video) besitzt. Nun haben wir vielleicht das Verhalten:
GIVEN video A is available WHEN the user rents the video A THEN the video A is marked as unavailable
definiert. Für alle, die noch nicht Teil 1 gelesen haben bietet sich jetzt die Chance kurz zurück zuspringen.
Unser Beispiel nutzt eine Datenbank, die alle Filme und deren Leih-Status beinhaltet. Schreiben wir also eine erste Version des glue-codes.:
@GIVEN("video A is available") public void available() { Video a = Videos.createNewVideo(); a.setName("A"); a.setAvailable(true); // write state back to the data-modell Videos.sync(a); } @WHEN("the user rents the video A") public void userRents() { user = Users.get("SomeOne"); video = Videos.get("A"); Videos.rent(user, video) } @THEN("the video A is marked as unavailable") { public void rentedVideoIsUnavailable() { video = Videos.get("A"); assertFalse(video.isAvailable()); }
Die Controller Users und Videos nutzen intern eine Datenbank, um die Daten persistent speichern zu können. Hier wollen wir die erste Test-Technik einführen, denn wir sehen jetzt vor der Herausforderung: Wie erhalten die Kontroller ihre Referenz auf die Datenbank?
Diskutieren wir erstmal mehrere Möglichkeiten aus:
1) Jeder Controller instantiiert seine eigene Datenbankverbindung wenn er erstellt wird:
public Videos() { this.database = new Database("localhost", 9876, "user", "password"); ... } Users() { this.database = new Database("localhost", 9876, "user", "password"); ... }
Hier vereinen wir gleich mehrere Anti-Pattern:
- Code-Duplizierung.
- Die Konfiguration der Datenbankverbindung steht statisch im Code.
- Zum Testen muss die Live-Datenbank verwaltet werden.
2) Singleton-Pattern
private Database(String host, int port, String loginName, String loginPassword) { } public static Database getInstance() { if (_instance == null) { _instance = new Database("localhost", 9876, "user", "password"); } return _instance; } public Videos() { this.database = Database.getInstance(); } public Users() { this.database = Database.getInstance(); }
Zwar ist die Initialisierung jetzt zentral, aber der Code ist immer noch nicht Testbar, da wir in einem Test keine Kontrolle über die verwendete Datenbank haben. Desweiteren ist die hier vorgestellte Version des Singleton-Patterns nicht sonderlich robust.
3) Inversion-of-Control mittels Dependency-Injection
Wir übergeben den Kontrollern die Datenbank-Referenz:
Videos(Database database) { this.database = database; } Users(Database database) { this.database = database; }
Nun können wir für die Tests einfach die Live-Datenbankanbindung durch einen Dummy ersetzen. Doch nun haben wir ein weiteres Problem: Wächst die Zahl der Abhängigkeiten einer Klasse so wird es schnell unübersichtlich jede Referenz von Hand zu verwalten: Beispielsweise könnte der Video-Kontroller für die rent-Funktion sowohl die Datenbankverbindung, als auch den User-Kontroller brauchen. Der User-Kontroller benötigt vielleicht auch eine Datenbankverbindung:
Database db = new Database(); Users users = new Users(db); Videos videos = new Videos(db, users, ...)
Hier helfen DI-Container (Dependency-Injection) wie beispielsweise picocontainer
pico = new DefaultPicoContainer(); pico.as(CACHE).addComponent(Database.class); // only one instance allowed pico.addComponent(Users.class); pico.addComponent(Videos.class); Videos videos = pico.getComponent(Videos.class)
Picocontainer beherscht DI via Constructor, setter, Annotations, uvm. Zum Testen müssen wir also nur noch die unerwünschte Live-Datenbank durch unsere Testdatenbank ersetzen:
pico = new DefaultPicoContainer(); pico.as(CACHE).addComponent(new TestDatabase()); pico.addComponent(Users.class); pico.addComponent(Videos.class); Videos videos = pico.getComponent(Videos.class)
Nun bleibt das Problem, dass wir immer noch eine Testdatenbank verwenden, die vor dem Test initialisiert werden muss und nach dem Test wieder aufgeräumt werden muss. Eine Lösung währe nun für den Test die Testdatenbank-Klasse zu überschreiben. Vielleicht einfach indem für jede Anfrage nur ein Dummy-Wert zurückgegeben wird. Bei großen Klassen kann das allerdings sehr aufwendig werden. Hier kommen wir zum zweiten Hilfsmittel: Mockup-Objekte mit Mockito.
Mockito ist ein Mocking-Framework, dass erlaubt zu einer gegebenen Klasse ein Dummy-Objekt zu erzeugen, welches alle Funktionen und Parameter des Originals besitzt allerdings ohne jegliche Funktionalität:
Enthält die Originalklasse beispielsweise die Funktion setAge(int age) { this.age = age; } so würde die Funktion in einem Mock einfach nur setAge(int age) {} lauten. Bringen wir also Picocontainer und Mockito zusammen, so erhalten wir unsere finale Version:
Users users; Videos videos; @Before setup { // Create database Mock Database mockDatabase = mock(Database.class); User someUser = new User("mockUser"); when(mockDatabase.get("SomeOne")).thenReturn(someUser); Video someVideo = new Video("mockVideo"); when(mockDatabase.get("A")).thenReturn(someVideo); // Initialize components via DI-Container pico = new DefaultPicoContainer(); pico.as(CACHE).addComponent(mockDatabase); pico.addComponent(Users.class); pico.addComponent(Videos.class); users = pico.getComponent(Users.class); videos = pico.getComponent(Videos.class); } @GIVEN("video A is available") public void available() { Video a = videos.createNewVideo(); a.setName("A"); a.setAvailable(true); // write state back to the data-modell videos.sync(a); } @WHEN("the user rents the video A") public void userRents() { user = users.get("SomeOne"); video = videos.get("A"); videos.rent(user, video) } @THEN("the video A is marked as unavailable") { public void rentedVideoIsUnavailable() { Video video = videos.get("A"); assertFalse(video.isAvailable()); }
Wir definieren eine Dummy-Datenbank, die bei bestimmten Anfragen vordefinierte Objekte zurückgibt. Desweiteren Deklarieren wir die Datenbank als "Cached" was generell bedeutet, dass jede Anfrage an den DI-Container die selbe Instanz erhält (vergleichbar mit einem Singleton-Objekt). Demnach testen wir für Users, Videos und Video die tatsächliche Implementierung, nutzen aber ein Mock-Objekt für die Datenbank. Möchte ich jetzt das verhalten gegen eine echte Datenbank testen, muss ich nur eine entsprechende Klasse initialisieren und dem DI-Container hinzufügen.
Natürlich kratzen wir hier nur an der obersten Schicht der Testing-Eisbergspitze, aber ich hoffe ich kann den einen oder anderen zum weiterlesen und lernen begeistern.
Weiterhin viel Spaß und wir sehen uns in Teil 3 der BDD Serie