In meinem letzten Post habe ich beschrieben, das Delegation für den Benutzer einer Klasse angenehm ist und das wir dies auch im realen Leben schätzen. Heute möchte ich kurz beschreiben wie sich das Einhalten des Gesetzes von Demeter auf das Schreiben von Unit-Tests und das Benutzen meiner Implementierungen auswirkt. Dazu habe ich das Modell um Implementierungen erweitert:
Am Beispiel von CityBike möchte ich erklären, dass die Implementierung der Unit-Test’s jetzt ganz einfach geht. Wir haben also die leere Implementierung der Klasse:
public class CityBike implements WithBasket { private Basket basket; public CityBike(Basket basket) { } public void addElement (Element e) { } public Basket getBasket () { return null; } }
Alle Implementierungen sind leer, da wir ja im Sinne von Test-Driven Development (TDD) zuerst unsere Tests schreiben und erst danach die Implementierung. Hier also die JUnit4-Testklasse:
import org.junit.Before; import org.junit.Test; import static org.junit.Assert.*; import static org.mockito.Mockito.*; public class CityBikeTest { private Basket basket; private CityBike cityBike; @Before public void setUp() { basket = mock(Basket.class); cityBike = new CityBike(basket); } @Test(expected=IllegalArgumentException.class) public void shouldNotCreateWithNullAsBasket() { new CityBike(null); } @Test public void shouldDelegateAddElementToBasket() { Element e1 = mock(Element.class); // cityBike.addElement(e1); // verify(basket).add(e1); } @Test public void shouldReturnBasket() { assertEquals(basket, cityBike.getBasket()); } }
Alle 3 Tests melden einen Fehler. Also ändern wir die Implementierung so, dass die Tests funktionieren:
public class CityBike implements WithBasket { private Basket basket; public CityBike(Basket basket) { if (null == basket){ throw new IllegalArgumentException("null as basket not allowed"); } this.basket = basket; } public void addElement (Element e) { this.basket.add(e); } public Basket getBasket () { return this.basket; } }
Alle Tests funktionieren. 🙂 (Die Implementierung und Tests für SimpleBasket und SimpleElement lasse ich hier im Text mal weg, damit es nicht zu unübersichtlich wird.) Damit haben wir als Programmierer von CityBike mal alles gemacht
Jetzt kommt ein zweiter Programmierer und möchte unsere Klassen benutzen. Es wird eine Person implementiert, die mit dem Fahrrad fahren soll. Allerdings muss dieser Fahrer seine Umhängetasche in den Korb (Basket) legen, bevor er losfahren darf. Natürlich sollte auch der zweite Programmierer seine Klassen “Fahrer” und “Tasche” testen. Hier die fertigen Implementierungen:
public class Fahrer { private Tasche tasche; private CityBike cityBike; public Fahrer(Tasche tasche, CityBike cityBike) { if (null == tasche){ throw new IllegalArgumentException("null as tasche not allowed"); } this.tasche = tasche; if (null == cityBike){ throw new IllegalArgumentException("null as cityBike not allowed"); } this.cityBike = cityBike; } void losfahren(){ this.cityBike.addElement(tasche); // aufsteigen // nach rechts und links schauen // anfangen in die Pedalen zu treten } } public class Tasche implements Element { public int getWeight() { return 12; } } import org.junit.Before; import org.junit.Test; import static org.mockito.Mockito.*; public class FahrerTest { private Tasche tasche; private CityBike cityBike; private Fahrer fahrer; @Before public void setUp() { this.tasche = mock(Tasche.class); this.cityBike = mock(CityBike.class); this.fahrer = new Fahrer(tasche, cityBike); } @Test public void shouldAddTascheInKorb() { // fahrer.losfahren(); // verify(cityBike).addElement(tasche); } }
Was passiert, wenn wir die Methode addElement(Element e) aus WithBasket und CityBike rausnehmen. Jeder der dann WithBasket, CityBike oder andere Subklassen von WithBasket benutzt muss dann den Wege über getBasket().add(Element e) gehen. Die Methode losfahren in Fahrer sieht dann so aus:
void losfahren(){ this.cityBike.getBasket().add(tasche); // aufsteigen // nach rechts und links schauen // anfangen in die Pedalen zu treten }
Aber nicht nur das wir jetzt in X Klassen dieses getBasket().add(tasche) stehen haben. Wir machen den Benutzern unserer Klasse auch noch das Testen schwieriger:
import org.junit.Before; import org.junit.Test; import static org.mockito.Mockito.*; public class FahrerTest { private Tasche tasche; private CityBike cityBike; private Basket basket; private Fahrer fahrer; @Before public void setUp() { this.tasche = mock(Tasche.class); this.cityBike = mock(CityBike.class); this.basket = mock(Basket.class); this.fahrer = new Fahrer(tasche, cityBike); } @Test public void shouldAddTascheInKorb() { when(cityBike.getBasket()).thenReturn(basket); // fahrer.losfahren(); // verify(basket).add(tasche); } }
Wir haben eine zusätzliche Membervariable basket im Test und der Test muss noch Vorbereiten das auf cityBike auch wirklich ein Basket geliefert wird. Und hier haben wir nur eine Verschachtelungstiefe. Man denke sich mal aus wie es aussieht wenn wir folgende Situation haben: A.getB().getC().getD().add(E e)
. Und jetzt stelle sich der Leser noch vor, dass das ganze Konsequent in der ganzen Applikation so gemacht wird. Irgendwann sind die Tests so aufwendig und kompliziert, dass kein Entwickler mehr Tests schreibt, weil einfach keine Zeit ist. Wenn dagegen immer das Law of Demeter eingehalten wird, dann sind die Tests kurz und einfach.
Schlusswort:
Ich habe hier Mockito verwendet um die Mock-Objekte zu schreiben. Eine wirkliche Empfehlung von meiner Seite. Diese Bibo ist der Hammer. Wirklich schnell zu lernen und eine wunderschöne API. Gratulation an die Entwickler zu diesem tollen Stück Software.
Schönes Wochenende!
Freut mich, dass dir Mockito gefaellt 😉
LikeLike
Hoi Patric,
ja, ist wirklich gut gemacht. Junit-Tests schreiben ist damit soooooo einfach. Gestern und vorgestern bin ich noch auf eine interessante Sache gestossen. Durch konsequentes TDD (mit Mockito und JUnit) ist bei mir im Domain-Code wie von selber eine DSL-ähnliche Struktur entstanden. Bis zur DSL war es dann nur noch eine Stunde.
Ich war so aufgeregt, dass ich die vorletzte Nacht fast nicht schlafen konnte. Wenn ich mal ein paar Minuten Zeit habe werde ich darüber schreiben.
Gruss Oliver
LikeLike