Und nun zu etwas ganz anderem: Ich habe wieder begonnen, zu programmieren.
Eines der Themen, das mich sehr fasziniert, ist „Behaviour Driven Development„. Dazu gibt es in Java einen netten Framework namens JBehave. Leider finde ich die Dokumentation zu JBehave etwas mager. Ich mag das Prinzip: „Make the simple things simple and the hard things possible“ (etwa: Mach die einfachen Dinge einfach und die schwierigen möglich).
Darum hier mein „Dankeschön“ an die Entwickler von JBehave: Eine möglichst einfach Bastelanleitung, wie JBehave in bestehende Maven-Projekte eingebaut werden kann. Im Moment arbeite ich auch mit Spring, also geht es hier darum, einen minimalen Startpunkt zu schaffen.
(Vielleicht finde ich auch noch die Zeit, ein Projekt ohne Spring zu beschreiben, doch so weit ich sehen kann, ist JBehave ohne Maven kaum verwendbar, oder wenigstens habe ich noch nicht herausbekommen, wie das sinnvoll funktioniert.)
Die Grundidee von BDD:
Praktisch jedes sinnvolle Software-Projekt besteht aus zwei Klassen von Menschen: Denen, die ein Problem haben, das sie mit Software lösen wollen, und den Menschen, die so gut programmieren können, dass sie die Lösung tatsächlich in Software gießen können. Wer in der Branche schon ein paar Tage verbracht hat, weiß, dass diese beiden Menschentypen sehr verschieden sein können, und dass darum Missverständnisse auf der Tagesordnung stehen können. BDD ist eines der Rezepte dagegen. Die Aufgabe für die erste Sorte Menschen ist, das gewünschte Verhalten (daher das „Behavior“ im Namen) – typischerweise in Form einer „Story“ – präzise zu formulieren. Die Aufgabe der Programmierer ist, aus diesem Verhalten zunächst einen oder mehrere automatische Tests abzuleiten und danach die Software so zu entwickeln, dass sie diesen Test erfüllt.
Für mich liegt der Reiz von BDD unter anderem darin, dass nach und nach das wesentliche Verhalten des Systems tatsächlich durch Tests abgesichert wird – so wird die Qualitätssicherung endlich weitgehend berechenbar, und Refactoring ist in einer geschützten Umgebung möglich.
Um mehr über BDD zu lernen, ist die oben zitierte Seite von Dan North („Behaviour Driven Development„) ein toller Einstieg.
JBehave Minimal
Ein minimaler JBehave Set-up benötigt darum drei Sorten von Dateien (naja… am Ende werden es fünf, Spring braucht noch eine weitere, und Maven auch. Aber jetzt geht es erst einmal nur um JBehave) . Die JBehave-Homepage zeigt diese drei sehr schön:
- Eine Datei mit „Stories“
- Eine Datei, die die Stories mit dem Test verknüpft
- „Klebstoff“, der die Konfiguration für den JBehave Test-Framework herstellt
So weit ist das JBehave-Tutorial auch ziemlich gut. Für mich war die zentrale Schwierigkeit: Welche Datei muss wohin, damit alles funktioniert? – Dazu rollen wir diese Liste von Dateien „von hinten“ auf: Zunächst sollte die Konfigurationsdatei erstellt werden.
Das JBehave-Tutorial beschreibt die Konfiguration hier, leider ist kein Beispiel ausgeführt.
Ich verwende eine abgewandelte Form aus dem Maven-Archetyp zu JBehave, die etwa so aussieht (hmmm… wenn ich hier öfter über Programmieren schreibe, brauche ich definitiv ein besseres Layout für Code… Sorry…):
package com.example.test;
import java.text.SimpleDateFormat;
import java.util.List;
import org.apache.log4j.Level;
import org.apache.log4j.Logger;
import org.jbehave.core.Embeddable;
import org.jbehave.core.configuration.Configuration;
import org.jbehave.core.configuration.MostUsefulConfiguration;
import org.jbehave.core.i18n.LocalizedKeywords;
import org.jbehave.core.io.CodeLocations;
import org.jbehave.core.io.LoadFromClasspath;
import org.jbehave.core.io.StoryFinder;
import org.jbehave.core.junit.JUnitStories;
import org.jbehave.core.model.ExamplesTableFactory;
import org.jbehave.core.parsers.RegexStoryParser;
import org.jbehave.core.reporters.StoryReporterBuilder;
import org.jbehave.core.steps.InjectableStepsFactory;
import org.jbehave.core.steps.ParameterConverters;
import org.jbehave.core.steps.ParameterConverters.DateConverter;
import org.jbehave.core.steps.ParameterConverters.ExamplesTableConverter;
import org.jbehave.core.steps.spring.SpringApplicationContextFactory;
import org.jbehave.core.steps.spring.SpringStepsFactory;
import org.springframework.context.ApplicationContext;
import static org.jbehave.core.io.CodeLocations.codeLocationFromClass;
import static org.jbehave.core.reporters.Format.CONSOLE;
import static org.jbehave.core.reporters.Format.HTML;
import static org.jbehave.core.reporters.Format.TXT;
import static org.jbehave.core.reporters.Format.XML;
public class UploadTaskStories extends JUnitStories {
public UploadTaskStories() {
configuredEmbedder().embedderControls().doGenerateViewAfterStories(true).doIgnoreFailureInStories(true).doVerboseFailures(true)
.doIgnoreFailureInView(true).useThreads(2).useStoryTimeoutInSecs(60);
}
@Override
public Configuration configuration() {
// Modify logging
Logger l = Logger.getRootLogger();
l.setLevel(Level.DEBUG);
ClassembeddableClass = this.getClass();
// Start from default ParameterConverters instance
ParameterConverters parameterConverters = new ParameterConverters();
// factory to allow parameter conversion and loading from external resources (used by StoryParser too)
ExamplesTableFactory examplesTableFactory = new ExamplesTableFactory(new LocalizedKeywords(), new LoadFromClasspath(embeddableClass), parameterConverters);
// add custom converters
parameterConverters.addConverters(new DateConverter(new SimpleDateFormat("yyyy-MM-dd")),
new ExamplesTableConverter(examplesTableFactory));
return new MostUsefulConfiguration()
.useStoryLoader(new LoadFromClasspath(embeddableClass))
.useStoryParser(new RegexStoryParser(examplesTableFactory))
.useStoryReporterBuilder(new StoryReporterBuilder()
.withCodeLocation(CodeLocations.codeLocationFromClass(embeddableClass))
.withDefaultFormats()
.withFormats(CONSOLE, HTML))
.useParameterConverters(parameterConverters);
}
@Override
public InjectableStepsFactory stepsFactory() {
// Die folgende Zeile an die tatsaechlichen Packages anpassen!!!
String path = "com.example.test".replaceAll("\.", "/");
// in der nächsten Zeile die Spring-Beans-Konfiguration richtig setzen:
ApplicationContext context = new SpringApplicationContextFactory(path+"/my_steps.xml").createApplicationContext();
return new SpringStepsFactory(configuration(), context);
}
@Override
protected List storyPaths() {
return new StoryFinder().findPaths(codeLocationFromClass(this.getClass()), "**/*.story", "**/excluded*.story");
}
}
Wichtig: Wo gehört diese Datei hin?
Gemäß der Konventionen von Maven landet diese Datei in einem Code-Verzeichnis. Ich verwende dafür entweder src/test/java/<package>/test
, oder ich lege ein separates Maven-Modul für Integrationstests an und lege diese Datei dann in src/main/java/<package>.test
. Die Datei heisst bei mir „<Thema>Stories.java“, denn sie konfiguriert, welche Stories zu diesem Thema verwendet werden sollen. Dieser Name wird uns unten in der Maven-Konfiguration noch einmal begegnen.
Außerdem ist die erste Zeile der Methode stepsFactory()
an die verwendeten Package-Namen anzupassen, und in der darauf folgenden Zeile die Datei für die Konfiguration der Spring-Beans anzugeben. Wir kommen bald darauf zurück, wie diese Datei auszusehen hat.
Die Stories…
… lege ich in einem Verzeichnis src/test/resources/test
ab. (Wie zuvor: Manchmal gibt es auch ein separates Integration-Test-Modul, dann landen die Stories im Verzeichnis src/main/resources/test
.) Die Story-Dateien sind im Tutorial schön beschrieben darum gibt’s hier nur ein super-kurzes Beispiel:
Scenario: DownloadTask has an empty download queue
Given an empty download queue
When the timer starts DownloadTask
Then a line is written to the logfile.
Die Datei könnte beispielsweise download_task.stories heissen.
Die Steps
der „Klebstoff“ zwischen Story und Test heisst bei JBehave „Step“. Die Steps sind im Tutorial auch richtig gut beschrieben, darum hier nur ein paar Worte als Appetithappen:
Jede „Given“-, „When“- oder „Then“-Zeile einer Story wird abgebildet auf eine Methode der Step-Klasse.
Die Step-Klasse sieht dann etwa so aus:
package com.example.test.steps;
import org.jbehave.core.annotations.Given;
import org.jbehave.core.annotations.Pending;
import org.jbehave.core.annotations.Then;
import org.jbehave.core.annotations.When;
public class DownloadTaskSteps {
@Given("an empty download queue")
@Pending
public void givenAnEmptyDownloadQueue() {
// PENDING
}
// ... weitere Methoden ...
}
Ein paar Dinge sind hier wichtig:
- Diese Methoden-Vorlage wurde von Maven generiert. Minimal ist einfach eine leere Klasse nötig, doch so ist einfacher zu erklären, was die Klasse tut 🙂
- Das Package heisst hier
<package>.test.steps
– konsequenterweise liegt die Klasse auch im entsprechenden Unterverzeichnis. - Es sind die Annotations, die über die Zuordnung zur Story entscheiden (der Name der Methode ist egal). Es gibt auch Annotations mit Parametern, doch für ein minimales Beispiel geht das zu weit.
- Tests, die nicht oder noch nicht fertig programmiert sind, können mit der Annotation
@Pending
„deaktiviert“ werden. „Pending“ Steps (oder auch Steps, die bisher noch nicht in Code gegossen sind) beeinflussen den Build-Erfolg in Maven nicht
Damit müsste JBehave schon einmal funktionieren – nur leider weiß Maven noch nichts von JBehave, und der Spring-Framework ist auch noch nicht auf die neuen Aufgaben eingeschworen.
Maven und das JBehave-Plugin
Für die noch-nicht-ganz-Fans von Maven: Maven kann den kompletten Build-Prozess von Java-Programmen organisieren. Dazu wird Maven durch eine Datei namens „pom.xml
“ konfiguriert. Die Details dieser pom.xml
sind andernorts ziemlich gut beschrieben, hier geht es also nur um die Einträge, die notwendig sind, um JBehave Leben einzuhauchen. Ich gebe zu, diesen Abschnitt habe ich nicht 1000%ig durchdacht. Fest steht: So funktioniert’s für mich.
Zunächst sollte man ein paar Properties setzen (um herauszukriegen, wo dieser Abschnitt in die pom.xml gehört, empfiehlt sich ein Blick in den POM Quick Overview):
<properties>
<jbehave.core.version>3.6.9</jbehave.core.version>
<jbehave.site.version>3.1.1</jbehave.site.version>
**/*Stories.java</embeddables>
</properties>
Die ersten beiden Zeilen sind nichts besonderes für JBehave. Wir werden einfach mehrere JBehave-Artefakte verwenden, und auf diese Weise können wir sicherstellen, dass wir immer zusammenpassende Versionen einsetzen.
Die Zeile zu den legt fest, welche Dateien die Konfiguration übernehmen.
Der Ablauf ist also etwa folgender: Mensch startet Maven, Maven ruft das JBehave-Plugin auf, das JBehave-Plugin sucht nach „Embeddables“ in der Maven-Konfiguration, findet so eine oder mehrere Konfigurationsdateien, und die Konfigurationsdateien wiederum bringen Stories und Steps zusammen.
Zurück zur pom.xml
. Den <dependencies>
dort werden drei Dependencies hinzugefügt, um JBehave „einzubauen“:
<dependency>
<groupId>org.jbehave</groupId>
<artifactId>jbehave-spring</artifactId>
<version>${jbehave.core.version}</version>
</dependency>
<dependency>
<groupId>org.jbehave</groupId>
<artifactId>jbehave-core</artifactId>
<version>${jbehave.core.version}</version>
<classifier>resources</classifier>
<type>zip</type>
</dependency>
<dependency>
<groupId>org.jbehave.site</groupId>
<artifactId>jbehave-site-resources</artifactId>
<version>${jbehave.site.version}</version>
<type>zip</type>
</dependency>
Hier tauchen also die oben festgelegten Properties für die Versionen wieder auf.
Zuguterletzt wird das JBehave-Plugin selbst konfiguriert. Das ist die Ecke, die ich noch nicht 100%ig verstanden habe. So funktioniert’s für mich (und nein, wenigstens der Abschnitt zum Logging ist ziemlich sicher nicht „minimal“. Sorry.):
<build>
<resources>
<resource>
<directory>src/test/java</directory>
<filtering>true</filtering>
<excludes>
<exclude>**/*.java</exclude>
</excludes>
</resource>
<resource>
<directory>src/test/resources</directory>
<filtering>true</filtering>
</resource>
</resources>
<plugins>
<plugin>
<groupId>org.jbehave</groupId>
<artifactId>jbehave-maven-plugin</artifactId>
<version>${jbehave.core.version}</version>
<executions>
<execution>
<id>unpack-view-resources</id>
<phase>process-resources</phase>
<goals>
<goal>unpack-view-resources</goal>
</goals>
</execution>
<execution>
<id>embeddable-stories</id>
<phase>integration-test</phase>
<configuration>
<!-- Die naechste Zeile legt fest, dass auch in src/test/... nach JBehave-Stories etc. gesucht wird: -->
<scope>test</scope>
<includes>
<include>${embeddables}</include>
</includes>
<addTestClassPath>true</addTestClassPath>
<excludes />
<threads>1</threads>
<metaFilters>
<metaFilter></metaFilter>
</metaFilters>
</configuration>
<goals>
<goal>run-stories-as-embeddables</goal>
</goals>
</execution>
</executions>
<dependencies>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.16</version>
</dependency>
</dependencies>
</plugin>
</plugins>
</build>
… und damit müsste die pom.xml erledigt sein.
Zuguterletzt: Spring!
Damit bleibt noch eine Datei, die Konfiguration für Spring. Die kann wieder bemerkenswert einfach sein, nämlich so:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:jee="http://www.springframework.org/schema/jee" xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-2.5.xsd
http://www.springframework.org/schema/jee http://www.springframework.org/schema/jee/spring-jee-2.5.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-2.5.xsd">
<!-- Section: Configuration and Spring set-up -->
<!-- <context:property-placeholder
location="classpath:/config.properties" /> -->
<!-- Section: Test Beans -->
<bean class="com.example.test.steps.DownloadTaskSteps" />
<!-- Section: Beans under test -->
<bean class="com.example.UploadTask" />
</beans>
Auch dies ist nicht wirklich eine minimale Konfiguration: Die XML-Namespace-Deklarationen im Header enthalten einige, die in diesem Mini-Beispiel nicht verwendet wurden. Doch ich finde sie nicht störend, und vielleicht sparen sie dem geneigten Leser Tipparbeit.
Ansonsten deklariert dieses Beispiel einfach nur eine Bean, die den Test enthält, und eine Bean für die Klasse, die getestet wird. Bei Bedarf kann die Properties-Datei in der beschriebenen Weise eingebunden werden. Das und vieles mehr sind einfach nur die Standard-Mechanismen von Spring.
Los!
… und wenn der geneigte Leser jetzt mvn integration-test
eingibt, müssten die ersten Test-Ergebnisse entstehen. Mehr Details dazu gibt’s hier, und einen wichtigen Tipp: Falls doch etwas schief geht, hilft die Option „-X“: Debugging-Ausgaben anschalten (also dann: mvn -X integration-test
)
Viel Spaß und viel Erfolg damit. Mit etwas Glück schaffe ich es in den nächsten Tagen noch, die wichtigsten Fehler-Situationen hier zu beschreiben.