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.

HTML5 und Javascript zur Darstellung von Graphen

Eingetragen bei: javascript, js | 0

Im folgenden Beispiel soll ein Graph dargestellt und kontinuierlich aktualisiert werden, dessen Daten asynchron von einer Web-Ressource nachgeladen werden. Hierfür kommt die HTML5/Javascript Library RGraph (http://www.rgraph.net/) zum Einsatz, die im privaten Gebrauch umsonst verwendet werden darf. Es ist lediglich ein Verweis auf die Web-Seite von RGraph notwendig. Auf dieser kann die aktuelle Version der Library in Form eines ZIP-Archives bezogen werden. Aus dem Verzeichnis RGraph/libraries werden für dieses Beispiel die folgenden Javascript-Dateien benötigt:

  • jquery.min.js
  • RGraph.common.core.js
  • RGraph.line.js

Diese werden in die HTML-Seite eingebunden:

<script type=“text/javascript“ src=“js/jquery.min.js“ />
<script type=“text/javascript“ src=“js/RGraph.common.core.js“ />
<script type=“text/javascript“ src=“js/RGraph.line.js“ />

Die Grafik wird im HTML5-Element Canvas integriert:

<canvas id=“myCanvas“ width=“600″ height=“250″>[No canvas support]</canvas>

Am Ende der HTML-Seite wird das nun folgende Javascript eingebunden. Zuerst werden die Graph-Daten mit null initialisiert:

d1=[];

for (var i=0; i<250; ++i) {
d1.push(null);
}

Anschließend wir die Funktion getGraph implementiert. Beim ersten Aufruf wird ein Line-Graph erstellt, spätere Aufrufe geben das bereits erzeugte Objekt zurück. Im Konstruktor wird die Id des Canvas und das Daten Array übergeben. Eine vollständige Liste aller möglichen Eigenschaften inklusive ihrer Default-Werte können in RGraph.line.js eingesehen werden:

function getGraph(id, d1)
{
if (!window.line) {
window.line = new RGraph.Line(id, d1);
window.line.Set(‚chart.xticks‘, 100);
window.line.Set(‚chart.title.xaxis‘, ‚Time >>>‘);
window.line.Set(‚chart.title.yaxis‘, ‚Number of Worker‘);
window.line.Set(‚chart.title.vpos‘, 0.5);
window.line.Set(‚chart.title‘, ‚Assigned Jobs‘);
window.line.Set(‚chart.title.yaxis.pos‘, 0.5);
window.line.Set(‚chart.title.xaxis.pos‘, 0.5);
window.line.Set(‚chart.colors‘, [‚red‘]);
window.line.Set(‚chart.linewidth‘,3);
window.line.Set(‚chart.yaxispos‘, ‚right‘);
window.line.Set(‚chart.ymax‘, 30);
window.line.Set(‚chart.xticks‘, 25);
}

return window.line;
}

Nun wird die drawGraph-Funktion erstellt, die einen asynchronen AJAX-Aufruf auf eine Web-Ressource durchführt. In diesem Beispiel liefert sie einen Number-Wert, der im Graph dargestellt werden soll. Auf das Ergebnis kann in der übergebenen Callback-Funktion zugegriffen werden:

function drawGraph() {
RGraph.AJAX(‚http://server_url‘, myCallback);
}

Die Callback-Funktion setzt zunächst das Canvas-Element zurück, um anschließend den Graphen aus der getGraph-Funktion neu zu zeichnen. Über this.responseText erhält man den Rückgabe-Wert der Web-Ressource, welcher zu einer Number konvertiert und anschließend im Daten-Array hinterlegt wird. Sofern das Daten-Array mehr als die maximale Anzahl von 250 Elementen besitzt, wird das erste Element entfernt, so dass die Größe des Graphen trotz fortlaufender Aktualisierung gleich bleibt. Anschließend wird das neue Daten-Array dem Graphen-Objekt zugewiesen und die drawGraph-Funktion mit einer Verzögerung von 100 ms erneut aufgerufen:

function myCallback() {
RGraph.Clear(document.getElementById(„myCanvas“));

var graph = getGraph(‚myCanvas‘, d1);
graph.Draw();
var data = this.responseText;
d1.push(Number(data));

if (d1.length > 250) {
d1 = RGraph.array_shift(d1);
}

if (document.all && RGraph.isIE8()) {
alert(‚[MSIE] Sorry, Internet Explorer 8 is not fast enough to support animated charts‘);
} else {
graph.original_data[0] = d1;
setTimeout(drawGraph, 100);
}
}

Zum Schluss muss die drawGraph-Funktion einmal Initial ausgeführt werden, um die Graphen-Darstellung zu starten:

drawGraph();

Das Resultat kann dann – abhängig von den Daten der Web-Ressource – wie folgt aussehen:chart_example

RGraph wird von allen gängigen Browsern in den jeweils aktuellen Versionen unterstützt. Auch der Internet Explorer akzeptiert ab Version 9 das HTML5-Tag Canvas.