Behavior Driven Development mit JBehave

Eingetragen bei: Java, test-driven development | 0

Behavior Driven Development (BDD) – ist eine Kombination aus Test-Driven-Development (TDD) und Domain-Driven-Design (DDD). TDD wird in der agilen Softwareentwicklung eingesetzt und ist ein Prozess, bei dem zuerst Testfälle konzeptioniert und erst im Anschluss die Funktionalität umgesetzt wird. Nähere Informationen hierzu, finden sich auch in meinem früheren Artikel http://martinzimmermann1979.wordpress.com/2011/12/09/testgetriebene-entwicklung-mit-junit-easymock-und-spring. Auch DDD orientiert sich an der agilen Softwareentwicklung, sieht aber die Fachlichkeit einer Anwendung im Vordergrund. Anforderungen werden in einer ubiquitären Sprache beschrieben, welche von allen am Softwareentwicklungsprozess beteiligten Personen verstanden wird.

Auch bei BDD kommt eine ubiquitäre Sprache zum Einsatz, um fachliche Anforderungen in sogenannten Szenarios zu definieren. Das hier definierte Verhalten wird später mit automatisierten Tests überprüft. Bei der Beschreibung werden Schlüsselworte verwendet, damit die Sprache von den Testfällen korrekt interpretiert werden kann:

Given: Vorraussetzungen für den Testfall
When: Aktion die ausgeführt wird
Then: Überprüft ob das erwartete Verhalten eingetreten ist

Nach einer Aktion kann zunächst das Zwischenergebnis geprüft und anschließend mit weiteren Aktionen und Prüfungen fortgefahren werden. Folgen mehrere Sätze des gleichen Typs, können sie mit and verknüpft werden.

Kommen wir nun zu einem konkreten Beispiel. Es besteht die fachliche Anforderung, dass ein vorhandener Benutzer aktiviert werden kann. Diese Funktionalität wird im UserService realisiert. Das zu testende Verhalten wird mit folgendem Szenario beschrieben:
jbehave_scenario

Das Szenario wird unter src/test/resources/de/mz/jbehave/user_scenarios.story gespeichert und wird dem Testfall als Grundlage zur Ausführung dienen.

Als BDD-Framework wird JBehave in der aktuellen Version 3.7.5 eingesetzt, welches bequem als Maven Dependency eingebunden werden kann:

<properties>
<jbehave.version>3.7.5</jbehave.version>
</properties>

<dependency>
<groupId>org.jbehave</groupId>
<artifactId>jbehave-core</artifactId>
<version>${jbehave.version}</version>
<scope>test</scope>
</dependency>

Nun wird der UserService erstellt, der folgende Methoden implementiert:

void addUser(User user);

User getUser(String name);

boolean activateUser(String name);

Der hier verwendete User besteht aus den zwei Eigenschaften:

private String name;
private boolean active;

Nun wird der Testfall src/test/java/de/mz/jbehave/UserActivationStory erstellt, der somit die gleiche  Verzeichnisstruktur besitzt, wie das zuvor erstellte Szenario. Es handelt sich um eine Unterklasse von Embedder. An den verwendeten Annotations ist bereits ersichtlich, wie die textuelle Beschreibung mit den implementierten Testfällen in Verbindung steht. Jeder Satz des Szenarios, ist in einer Annotation der UserActivationStory wiederzufinden:

public class UserActivationStory extends Embedder {

private UserService userService = new UserServiceImpl();

@Given(„a user with the name $name“)
public void addUser(String name) {
User user = new User();
user.setName(name);
userService.addUser(user);
}

@When(„the user with the name $name will be activated“)
public void activateUser(String name) {
userService.activateUser(name);
}

@Then(„the user with the name $name should be active“)
public void userShouldBeActive(String name) {
User user = userService.getUser(name);
Assert.assertEquals(name, user.getName());
Assert.assertTrue(user.isActive());
}
}

Um die Wiederverwendbarkeit der Test-Methoden zu erhöhen, können Parameter benutzt werden. Innerhalb der Annotation werden sie mit dem Dollar-Symbol gekennzeichnet und stehen in der Test-Methode als Übergabe-Parameter zur Verfügung. Bei der Szenario-Beschreibung wird einfach der entsprechende Wert angegeben, in obigen Beispiel also mustermann.

Im gleichen Package wie der Testfall wird nun ein Runner zum Ausführen des selbigen erstellt. Dieser erbt von JUnitStory und benötigt eine Configuration und eine StepFactory. JBehave verwendet das Convention over Configuration Prinzip, so dass hier nur Einstellungen konfiguriert werden müssen, die nicht der Norm entsprechen. Will man während der Testausführung Meldungen auf der Konsole erhalten, muss ein StoryReportBuilder definiert werden. Als StepFactory wird die InstanceStepsFactory verwendet, welche die Configuration und die auszuführende Story-Klasse benötigt:

public class UserScenarios extends JUnitStory {

@Override
public Configuration configuration() {
return new MostUsefulConfiguration()
.useStoryReporterBuilder(new StoryReporterBuilder().withDefaultFormats().withFormats(Format.CONSOLE, Format.TXT));
}

@Override
public InjectableStepsFactory stepsFactory() {
return new InstanceStepsFactory(configuration(), new UserActivationStory());
}
}

Ohne weitere Konfiguration, wird die Szenario-Beschreibung im gleichen Package wie der Testfall gesucht. Möchte man dies ändern, so kann ein anderer StoryLoader verwendet werden:

configuration.useStoryLoader(new LoadFromURL(„pathToResource“));

Für dieses Beispiel ist das aber nicht nötig, so dass die Klasse UserScenarios nun als JUnit-Test ausgeführt werden kann. Im Anschluss befindet sich im target unter jbehave/view die Datei reports.html, die Auskunft über den Erfolg des Testfalls gibt.

Sollen die JBehave Testfälle automatisch beim Bauen mit maven ausgeführt werden, so kann das jbehave-maven-plugin verwendet werden. Hier die Anpassungen in der pom.xml:

<build>
<finalName>de.mz.jbehave</finalName>
<plugins>
<plugin>
<groupId>org.jbehave</groupId>
<artifactId>jbehave-maven-plugin</artifactId>
<version>${jbehave.version}</version>
<executions>
<execution>
<id>run-stories-as-embeddables</id>
<phase>integration-test</phase>
<configuration>
<scope>test</scope>
<includes>
<include>**/*Scenarios.java</include>
</includes>
</configuration>
<goals>
<goal>run-stories-as-embeddables</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>

Sofern sich die Test-Klassen in src/test/java befinden, muss der Scope explizit auf test gesetzt werden. Andernfalls werden nur Klassen in src/main/java gefunden. Inkludiert werden alle Java-Klassen die auf Scenarios enden. Nun kann JBehave als maven goal ausgeführt werden:

mvn integration-test

Leider sind die Reports komplett unformatiert und werden ohne css-Styles generiert. Um grafisch ansprechende Reports zu erzeugen, wird das maven-dependency-plugin benötigt, welches zwei hierfür notwendige JBehave Artefakte lädt:

<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<executions>
<execution>
<id>unpack-jbehave-site-resources</id>
<phase>generate-resources</phase>
<goals>
<goal>unpack</goal>
</goals>
<configuration>
<artifactItems>
<artifactItem>
<groupId>org.jbehave.site</groupId>
<artifactId>jbehave-site-resources</artifactId>
<version>3.1.1</version>
<type>zip</type>
<outputDirectory>${project.build.directory}/jbehave/view</outputDirectory>
</artifactItem>
<artifactItem>
<groupId>org.jbehave</groupId>
<artifactId>jbehave-core</artifactId>
<version>${jbehave.version}</version>
<outputDirectory>${project.build.directory}/jbehave/view</outputDirectory>
</artifactItem>
</artifactItems>
</configuration>
</execution>
</executions>
</plugin>

Nach erneutem Ausführen der Integrations-Tests sieht der Report nun so aus:jbehave_statistic

Sofern die Eclipse IDE mit dem m2e Plugin verwendet wird, erscheint nach Anpassung der pom.xml folgender Fehler in der Problem View:

maven-dependency-plugin (goals „copy-dependencies“, „unpack“) is not supported by m2e.

Über die Option „Permanently mark goal unpack in pom.xml as ignored in Eclipse build“ lässt sich der Fehler ignorieren. Die Funktionalität wird hierdurch nicht beeinträchtigt. Will man JBehave in einem bestehenden Projekt einsetzten, benötigt man zumeist eine Spring Integration. Hierfür werden zwei weitere Maven Dependencies benötigt:

<properties>
<spring.version>3.1.1.RELEASE</spring.version>
</properties>

<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>${spring.version}</version>
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.jbehave</groupId>
<artifactId>jbehave-spring</artifactId>
<version>${jbehave.version}</version>
</dependency>

Im Folgenden werden zwei Spring-Kontexte erstellt, der eine kommt während der Laufzeit zum Einsatz, während der andere für die Integrations-Tests benötigt wird. Zunächst wird unter src/main/resources/applicationContext.xml der Laufzeit-Kontext erstellt. Er beinhaltet den zu testenden UserService und aktiviert das autowiring:

<context:annotation-config />

<bean id=“userService“ class=“de.mz.jbehave.UserServiceImpl“/>

Für die Integrations-Tests wird unter src/test/resources/applicationTestContext.xml ein weiterer Kontext erstellt, der die UserActivationStory beinhaltet und den Laufzeit-Kontext importiert. So ist gewährleistet, dass Laufzeit- und Test-Kontext sich bis auf die zusätzlichen Test-Klassen gleichen:

<import resource=“classpath:applicationContext.xml“/>

<bean id=“userActivationStory“ class=“de.mz.jbehave.UserActivationStory“/>

Nun wird die Runner-Klasse UserScenarios angepasst. Statt der InstanceStepsFactory wird die SpringStepsFactory verwendet, die zusätzlich zur Configuration den Spring-Test-Kontext erhält. Die configuration-Methode kann aus obigem Beispiel übernommen werden:

@Override
public InjectableStepsFactory stepsFactory() {
ApplicationContext context = new SpringApplicationContextFactory(„classpath:applicationTestContext.xml“).createApplicationContext();
return new SpringStepsFactory(configuration(), context);
}

Die Klasse UserActivationStory erstellt nun den UserService nicht mehr selber, sondern erhält ihn per Dependency Injection:

@Autowired
private UserService userService;

So lassen sich Integration-Tests mittels JBehave auch in Spring-Projekten ausführen, so dass das BDD-Framework in vielen Projekten einfach zu integrieren ist.

Zur Unterstützung beim Erstellung von Szenarios, kann das JBehave Eclipse Plugin installieren werden http://jbehave.org/reference/eclipse/updates/. Bei mir führte dies allerdings wiederholt zu einem einfrieren von Eclipse, so dass ich mich von dem Plugin wieder getrennt habe.

Testgetriebene Entwicklung mit JUnit, EasyMock und Spring

Eingetragen bei: Java, test-driven development | 0

Die testgetriebene Entwicklung – test-driven development (TDD) – wird häufig in der agilen Softwareentwicklung eingesetzt. Hierbei werden zunächst die Testfälle konzeptioniert und erst anschließend die gewünschte Funktionalität umgesetzt. Aufgrund fehlender Umsetzung werden daher die Testfälle zunächst zwangsläufig fehlschlagen. Erst mit sukzessiver Funktions-Implementierung werden die Testfälle nach und nach erfolgreich durchlaufen.

Am einfachsten zu Testen sind Methoden, die für unterschiedliche Übergabe-Parameter entsprechende Rückgabe-Werte liefern wie z.B. statische Utility-Methoden. Um diese Methoden zu testen wird JUnit verwendet, welches unter http://www.junit.org/ in der aktuellen Version 4.10 zum Download bereit liegt. Alternativ kann man sich JUnit auch als Maven Dependency laden:

<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.10</version>
<scope>test</scope>
</dependency>

Das Testen einer Utility-Methode mit JUnit sieht dann folgendermaßen aus:

assertTrue(MyUtility.returnTrue(„parameter“));

Sofern die zu testende Methode Aufgaben an weitere Klassen delegiert, ist es nicht mehr möglich ausschließlich JUnit Tests zu verwenden, ohne eine vollständige Anwendungs-Umgebung zu initialisieren. Stattdessen müssen zusätzlich alle verwendeten Klassen mit initialisiert und getestet werden.

Hierbei handelt es sich dann nicht mehr um Modultests, sondern um Integrationstests, die definitiv wichtig sind und ihre Berechtigung haben, bei der testgetriebenen Entwicklung aber nicht im Vordergrund stehen. Hier soll nur das zu entwickelnde Modul getestet werden.

Daher werden bei der testgetriebenen Entwicklung Mocking-Frameworks verwendet, damit alle Klassen die in der zu testenden Methode verwendet werden, durch sogenannte Mock-Objekte ersetzt werden können. Diese Mocks bieten die Möglichkeit zu testen, ob die erwarteten Methode-Aufrufe mit den definierten Übergabe-Parametern erfolgen. Findet der Aufruf nicht statt oder ein nicht erwarteter Aufruf, so schlägt der Testfall fehl. Die Funktionalität der gemockten Objekte wird also nicht mit getestet, sondern kann in separaten Testfällen geprüft werden.

Als Mocking-Framework wird im folgenden Beispiel EasyMock verwendet, welches unter http://easymock.org/ in der aktuellen Version 3.1 verfügbar ist. Auch EasyMock ist als Maven Dependency erhältlich:

<dependency>
<groupId>org.easymock</groupId>
<artifactId>easymock</artifactId>
<version>3.1</version>
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.easymock</groupId>
<artifactId>easymockclassextension</artifactId>
<version>3.1</version>
<scope>test</scope>
</dependency>

Die erste Library wird zum Mocken von Interfaces benötigt, was als best practice anzusehen ist. Ist es dennoch mal nötig eine Klasse zu mocken, weil man z.B. bestehenden Code nachträglich testen möchte, kann die EasyMock-Classextension verwendet werden.

Kommen wir nun zu einem kleinen Beispiel. Es wird ein UserService getestet der einen User erhält und diesen mittels eines UserDAOs speichern soll. Hierbei wird der UserDAO gemockt und es soll überprüft werden, ob die save-Methode des DAOs auch wirklich aufgerufen wird.

Auch wenn bei der testgetriebenen Entwicklung zuerst der Testfall erstellt werden sollte, folgt zum besseren Verständnis zunächst der UserService und erst anschließend die Testfälle:

public class UserService {

private UserDAO userDAO;

public User save(User user) {
return userDAO.save(user);
}

public void setUserDAO(UserDAO userDAO) {
this.userDAO = userDAO;
}
}

Zuallererst wird ein Setup initialisiert, in welchem der DAO gemockt und die erwarteten Methoden-Aufrufe definiert werden. Die Annotation @Before besagt hierbei, dass das Setup vor der Ausführung jedes einzelnen Testfalls in dieser Test-Klasse ausgeführt werden soll.

@Before
public void setUp() {
name = „userName“;
user = new User(name);
userDAO = createMock(UserDAO.class);
expect(userDAO.save(user)).andReturn(user); // (1)
replay(userDAO);

userService = new UserService();
userService.setUserDAO(userDAO);
}

Wie bereits erwähnt wird der UserDAO gemockt und die Erwartung ausgesprochen, dass die save-Methode mit dem gegebenen User aufgerufen wird. Bei der expect-Methode handelt es sich um eine statische Methode von EasyMock, die verwendet wird, wenn der erwartete Methode-Aufruf einen Return-Value besitzt. Über die andReturn-Methode kann anschließend der Rückgabe-Wert definiert werden, den das Mock-Objekt zurückgeben soll.

Hätte die save-Methode keinen Rückgabe-Wert, sähe die Stelle (1) folgendermaßen aus:

userDAO.save(user);

Der expect-Aufruf würde also einfach weggelassen. Der replay-Aufruf in der darauf folgenden Zeile besagt, dass keine weiteren Aufrufe auf dem UserDAO-Mock erwartet werden und nun nur noch die Aufrufe folgen, die bei Test-Ausführung getätigt werden.

Nun muss der UserService noch den UserDAO-Mock gesetzt bekommen und wir können zum eigentlich Test kommen. Hierbei sollte man sich nicht wundern, das Setup ist meist deutlich länger als der eigentliche Test.

@Test
public void shouldSaveUser() {
User save = userService.save(user);
verify(userDAO);
assertNotNull(save);
assertEquals(„userName“, save.getName());
}

Hier wird die save-Methode des UserService aufgerufen und anschließend die verify-Methode von EasyMock aufgerufen. Damit wird überprüft ob die zuvor definierten Mock-Aufrufe auch wirklich getätigt werden oder nicht. Anschließend kann der Rückgabe-Wert wie gehabt mit JUnit überprüft werden.

Um EasyMock etwas genauer zu verstehen modifizieren wir nun die Zeile (1) der Setup-Methode:

expect(userDAO.save(new User(„userName“))).andReturn(user);

In der Setup-Methode wird dem UserDAO-Mock also ein anderer User übergeben, als dem UserService im Test, lediglich der Namen ist gleich. Da nur der Name auf Gleichheit überprüft wird, läuft der Testfall wie gehabt durch. Fügen wir nun dem Testfall noch folgende Zeile hinzu:

assertEquals(save, user);

Hierauf hin wird der Testfall fehlschlagen. Grund dafür ist, dass EasyMock die equals-Methode der User-Klasse überprüft, um zu Testen ob der Übergabe-Parameter gleich ist. Sofern die equals-Methode nicht überschrieben wurde, wird die equals-Methode von Object verwendet, die auf gleiche Referenz testet. Da hier nun zwei unterschiedliche Objekte mit lediglich den gleichen Property-Werten verwendet werden, schlägt der Testfall fehl. Sobald allerdings die equals-Methode von der User-Klasse überschrieben wird und hierbei auf Namens-Gleichheit getestet wird, ist der Test-Durchlauf wieder erfolgreich.

Dies Verhalten ist auch der Grund, warum ich auf einen weiteren wichtigen Bestandteil von EasyMock kommen möchte, das Capture.

Stellen wir uns vor, der UserService reicht das User-Objekt nicht direkt zum UserDAO durch, sondern erstellt aufgrund des Users ein UserEntity, welches vom UserDAO konsumiert wird. Die Properties des Users sind hierbei identisch mit denen des UserEntities. Die Implementierung sieht also so aus:

public User save(User user) {
UserEntity userEntity = new UserEntity(user.getName());
UserEntity entity = userDAO.save(userEntity);
User result = new User(entity.getName());
return result;
}

Sofern das UserEntity viele Properties beinhaltet, müßte für den Testfall also ein exakt gleiches UserEntity erstellt werden, damit das UserEntity im Testfall und das im UserService erzeugte von EasyMock als gleich anerkannt wird. Sofern die equals-Methode nicht überschrieben werden dürfte, würde der Testfall sogar immer fehlschlagen, da es sich zwangsläufig um zwei unterschiedliche Referenzen handeln würde. Aber auch so ein Fall lässt sich problemlos mit EasyMock abbilden.

Hierzu muss lediglich der übergebene User durch ein Capture ersetzt werden:

captureUser = new Capture<UserEntity>();
userDAO = createMock(UserDAO.class);
expect(userDAO.save(capture(captureUser))).andReturn(userEntity);
replay(userDAO);

Es wird ein Capture Objekt erzeugt, welches nicht direkt in die save-Methode des UserDAOs gegeben wird. Stattdessen wird hier die EasyMock-Methode capture aufgerufen. Nun kann im Test über das Capture auf das dem Mock übergebene Objekt zugegriffen und die Werte überprüft werden:

assertNotNull(captureUser.getValue());
UserEntity userEntity = captureUser.getValue();
assertEquals(„userName“, userEntity.getName());

Mit diesem Funktionsumfang von EasyMock lassen sich bereits die meisten Test-Szenarien abdecken. Ab und an wird man in spezial Fällen Lösungen in der EasyMock-Dokumentation suchen müssen, für die meisten Anwendungsfälle dürfte es aber genügen.

Nun noch ein kleiner Exkurs über die Modultest hinaus zu den Integrationstests. Sofern man Spring verwendet kann es sehr nützlich sein, für Testfälle einen Spring-Context hochzuziehen. Folgende Spring-Library ermöglicht dies:

<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>3.0.6.RELEASE</version>
<scope>test</scope>
</dependency>

Nun legt man den zu testenden Service an und definiert diesen in der Spring-XML Konfiguration, welche sich im Classpath befinden sollte:

<version=“1.0″ encoding=“UTF-8″?>
<beans>
<context:annotation-config/>
<bean id=“myService“ class=“MyService/>
</beans>

Nun kann man die XML-Konfiguration über die Annotation ContextConfiguration angeben und muss anschließend die Runner Klasse definieren. Da in der Spring-Konfiguration Annotations aktiviert wurden, kann nun der Service direkt in den Testfall per Autowire-Annotation injected werden:

@ContextConfiguration(locations = { „/test.xml“ })
@RunWith(SpringJUnit4ClassRunner.class)
public class SpringContextTest {

@Autowired
private MyService myService;

@Test
public void shouldExecuteMyService() {
assertNotNull(myService);
assertTrue(myService.doWhatYouHaveTodo());
}
}

Über die testgetriebene Entwicklung wird immer wieder gesagt, dass sie zu langsam und damit zu teuer ist. Dies konnte ich bislang nicht feststellen. Sicherlich ist es zunächst mehr Aufwand testgetrieben zu Entwickeln, aber die Einarbeitung in jedes neue Framework kostet zunächst einmal Zeit und bringt es mit sich, sich bei neuen Gegebenheiten erneut mit der API und deren Dokumentation zu beschäftigen. Der Zeitaufwand der beim Erstellen der Testfälle selber benötigt wird, ist überschaubar und wird durch die direkt bemerkten Flüchtigkeitsfehler wieder wett gemacht. Es muss nicht umständlich ein Server gestartet werden, nur um dann doch wieder ein Fehlverhalten festzustellen. Es ist grundsätzlich schneller einen Testfall auszuführen, als bei einer Software manuell einen ganz bestimmten Zustand zu testen.