Der erste Teil dieses Artikels hat gezeigt, wie Mock-Objekte prinzipiell das Leben von Java-Web-Entwickler erleichtern. Nun folgt ein Beispiel aus der Praxis.
Dass die durchgängige Entwicklung von Mock-Objekten relativ viel Arbeit bedeutet, wurde schon im ersten Teil erwähnt. Umso wichtiger ist es, dass die dafür notwendige technische Infrastruktur möglichst einfach umzusetzen ist.
Je nachdem, welche Kombination von Frontend- und Backend-Technologie man einsetzt, sehen die konkreten Lösungen, wie man ein Frontend an Mock-Objekte anschließt, unterschiedlich aus. Das Frontend selbst sollte in jedem Fall unabhängig davon bleiben, ob es mit Mock-Objekten oder gegen die richtigen Dienste läuft. Schließlich dürfen die Mock-Objekte den produktiven Betrieb auf keinen Fall stören.
Damit die Umsetzung einer Mock-Infrastruktur etwas anschaulicher wird, beschreibt dieser Artikel daher ein konkrektes Beispiel auf der Grundlage von Apache Wicket mit EJBs als Backend-Technologie.
Der Anwendungsfall
Der Anwendungsfall, der hier als Beispiel dient, ist zwar nicht ganz realistisch, dafür anschaulich: Angenommen, das Backend stellt eine Schnittstelle mit dem Namen AuthenticationService
zur Verfügung. Eine EJB-Komponente implementiert diese Schnittstelle, um einen Benutzer anhand eines Namens und eines Kennworts zu authentifizieren. Der Service bietet dazu die folgende Methode:
@Local public class AuthenticationService { public User authenticate(String username, String password) throws AuthenticationException; }
Die Exception wird geworfen, wenn die Authentifizierung scheitert, beispielsweise wenn der Benutzername bzw. das Kennwort nicht stimmt, wenn das Benutzerkonto gesperrt ist oder wenn ein technischer Fehler vorliegt. Die AuthenticationException
-Klasse liefert auch die Ursache für den Fehler, sodass die UI mit einer entsprechenden Fehlermeldung darauf reagieren kann. Wenn allerdings ein Benutzer erfolgreich angemeldet werden konnte, liefert der Service ein User
-Objekt zurück.
Dependency-Injection von EJB-Komponenten
Wicket bietet von Haus aus die Möglichkeit, per Dependency Injection auf EJB-Komponenten zuzugreifen. Dazu verwendet man, wie in einer reinen JEE-Umgebung üblich, die @EJB-Annotation. Wicket kümmert sich dann darum, dass zur Laufzeit Bean-Instanzen zur Verfügung gestellt werden. Die dazu benötigte Erweiterung findet man bei Wicketstuff in Form des Java EE Inject-Moduls. Wie man die EJB-Dependency-Injection einrichtet, findet man z.B. als Anleitung bei JBoss.
Hier ein einfaches Beispiel:
public class LoginPanel extends Panel { @EJB private AuthenticationService authenticationService; // Aufruf der authenticate-Methode des Services in einer onSubmit-Methode }
Wenn diese Komponente innerhalb eines JEE-Anwendungsservers läuft, wird als Authentifierungsdienst automatisch die passende EJB-Komponente augewählt.
Das Ziel ist nun, das Frontend unabhängig von einer JEE-Laufzeitumgebung zu starten. Wenn die Webanwendung statt im EJB-Container nur in einem Web-Container wie Tomcat oder Jetty läuft, stehen keine EJBs zur Verfügung. Daher benötigt man eine Infrastruktur, die den Wicket-Komponenten Mock-Objekte anstelle der EJB-Komponenten zur Verfügung stellt. Dazu sind mehrere Schritte notwendig, die als nächstes beschrieben werden.
Die Mock-Implementierung
Aber der Reihe nach. Zunächst zum Mock-Objekt selbst. Wie könnte ein Mock von AuthenticationService
aussehen? Hier ein minimales Beispiel:
public void AuthenticationServiceMock implements AuthenticationService { private static final String USERNAME_INVALID = "user_invalid"; private static final String USERNAME_LOCKED = "user_locked"; private static final String USERNAME_TECHNICAL_ERROR = "user_error"; public User authenticate(String username, String password) throws AuthenticationException { if (username.equals(USERNAME_VALID)) { return new User(username); } else if (username.equals(USERNAME_LOCKED)) { throw new AuthenticationException(Reason.USER_LOCKED); } else if (username.equals(USERNAME_TECHNICAL_ERROR)) { throw new AuthenticationException(Reason.TECHNICAL_ERROR); } else { throw new AuthenticationException(Reason.USER_OR_PASSWORD_WRONG); } } }
Je nach Benutzername („user_invalid“, usw.) verhält sich die Mock-Implementierung unterschiedlich. Die vom Mock-Objekt vorgegebenen Benutzernamen führen zu fest vorgegebenen Zuständen. Auf diese Weise lassen sich alle möglichen Fehlerzustände, auf die der Anmeldedialog reagieren können muss, leicht herbeiführen.
Dependency-Injection für die Mock-Objekte
Jetzt muss Wicket dazu gebracht werden, das Mock-Objekt anstelle der EJB-Komponente einzubinden.
Das Dependecy-Injection in Wicket ist modular und leicht erweiterbar. Wicket bietet als Grundlage die Schnittstelle IComponentInstantiationListener
sowie die abstrakte Klasse Injector
an. Für EJB-Komponenten ist in Wicket die Klasse JavaEEComponentInjector
verantwortlich.
Die Initialisierung des Dependency-Injection geschieht in der init
-Methode der WebApplication
-Klasse der Anwendung. Hier wählt man explizit die Strategie, wie Wicket Abhängigkeiten auflösen und bereitstellen soll. Angenommen, die WebApplication
-Klasse heißt MyApplication
. Die Initialisierung könnte dann so ausschauen:
public class MyApplication extends WebApplication { protected void init() { super.init(); getComponentInstantiationListeners().add(getComponentInstantiationListener()); // Übrige Initialisierung } protected IComponentInstantiationListener getComponentInstantiationListener() { // Im Produktivbetrieb erhalten die Komponenten Referenzen auf die EJB-Komponenten return new JavaEEComponentInjector(this); } }
Die Auswahl der Dependency-Injection-Strategie ist hier bewusst in eine überschreibare Methode ausgelagert, damit man sie in einer abgeleiteten Klasse verändern kann. Um anstelle von EJBs die Mock-Objekte zu injizieren, wird eine eigene Implementierung von IComponentInstantiationListener
benötigt:
public class MyMockApplication extends MyApplication { @Override protected IComponentInstantiationListener getComponentInstantiationListener() { // Im Mock-Betrieb werden Referenzen auf Mock-Objekte geliefert statt auf EJB-Komponenten return new MockInjector(this); } }
Diese Mock-Applikation gibt an Stelle des JavaEEComponentInjector
einen MockInjector
zurück, der mit Hilfe einer MockFactory die Mock-Objekte injizieren soll:
public class MockInjector extends Injector implements IComponentInstantiationListener { private MockFactory factory; public MockInjector(WebApplication webApplication) { bind(webApplication); factory = new MockFactory(); // Unsere eigene Mock-Factory wird eingerichtet (s.u.) } @Override public void inject(Object object) { inject(object, factory); // Der Aufruf wird an die geeignete Methode der Oberklasse delegiert } @Override public void onInstantiation(Component component) { inject(component); // Der Aufruf wird an die geeignete Methode der Oberklasse delegiert } }
Dieser MockInjector
erbt von der Wicket-Klasse Injector
und muss hier nur dafür sorgen, dass alle Aufrufe an eine (ebenfalls selbst geschriebene) MockFactory
weitergeleitet werden. Diese Factory hat die Aufgabe, die mit @EJB-Annotationen versehenen Felder der Wicket-Komponenten mit geeigneten Mock-Objekte zu befüllen:
public class MockFactory implements IFieldValueFactory { private AuthenticationService authenticationService = new AuthenticationServiceMock(); @Override public boolean supportsField(Field field) { // Die Mock-Factory unterstützt nur @EJB-Annotationen return field.isAnnotationPresent(EJB.class); } @Override public Object getFieldValue(Field field, Object fieldOwner) { // Das übergebene Feld des übergebenen Objektes muss mit einem Mock-Objekt // belegt werden. Dazu muss diese Methode das passende Mock-Objekt liefern. if (field.getType().isAssignableFrom(AuthenticationService.class)) { // Falls das Feld vom Typ AuthenticationService ist, wird der passende Mock gewählt. return authenticationService; } } }
Diese Mock-Factory wird immer dann aufgerufen, wenn Wicket bei der Initialisierung einer Komponente auf eine @EJB-Annotation trifft. Die Klasse kapselt somit die Entscheidung, welches Mock-Objekt verwendet werden soll, wenn in einer UI-Komponente eine Schnittstelle referenziert wird. In unserem Beispiel prüft die Mock-Factory entsprechend, ob das Feld vom Typ AuthenticationService
ist. Wenn ja, dann wird die Instanz des AuthenticationService
Mock zurückgeliefert.
Starten eines Web-Containers mit den Mock-Objekten
Die Infastruktur für den Betrieb der Webanwendung mit den Mock-Objekten ist jetzt vollständig. Jetzt bleibt noch zu klären, wie man die Web-Anwendung möglichst unkompliziert im Mock-Betrieb starten kann. Denn damit Funktionen wie Hot-Code-Replacement möglichst gut funktionieren, soll es ja gerade nicht notwendig sein, die Anwendung in einem Server zu deployen. Besser wäre es, wenn man die Anwendung direkt aus der eigenen IDE heraus starten könnte.
Hier bietet sich Jetty an – ein Webserver, der sich auch eingebettet aus eigenem Code heraus starten lässt. Der folgende Code-Abschnitt zeigt, wie einfach sich Jetty aus einer main-Methode heraus hochfahren lässt:
public class MockApplicationStarter { public static void main(String[] args) throws Exception { // Einrichten des Servers auf dem Standard-Port 8080 Server server = new Server(); SocketConnector connector = new SocketConnector(); // Setzen von hohen Timeout-Werten, um das Debuggen zu vereinfachen connector.setMaxIdleTime(1000 * 60 * 60); connector.setSoLingerTime(-1); connector.setPort(8080); server.setConnectors(new Connector[] { connector }); // Definition der Web-Anwendung WebAppContext webAppContext = new WebAppContext(); webAppContext.setServer(server); webAppContext.setContextPath("/"); webAppContext.setResourceBase("target/myApplication"); // Zugriffspfad zu den Web-Ressourcen webAppContext.setParentLoaderPriority(true);// Umgeht mögliche Probleme mit dem Klassenpfad webAppContext.addOverrideDescriptor("target/test-classes/web.xml"); // Verweis auf web.xml-Fragment zum Starten der Mock-Anwendung server.setHandler(webAppContext); // Start des Web-Servers server.start(); // Herunterfahren des Web-Servers nach eine Eingabe auf der Konsole System.in.read(); server.stop(); server.join(); } }
Mehrere Annahmen kommen hier zum Tragen: Alle Java-Klassen müssen im Klassenpfad erreichbar sein, zudem müssen alle Web-Ressourcen (also die web.xml, Bilder, etc.) unter dem relativen Pfad target/myApplication
liegen. Dies ist z.B. der Fall, wenn die Anwendung durch Maven gebaut wird und im POM „myApplication“ als name der Web-Anwendung eingetragen ist.
Um die Web-Anwendung im Mock-Betrieb zu starten, muss Wicket die Klasse MyMockApplication starten. Dieser Eintrag steht in der web.xml. Man könnte nun die Original-web.xml-Datei kopieren und nur den Klassennamen ändern. Dies hätte aber zur Folge, dass alle sonstigen Einstellungen redundant vorliegen und gepflegt werden müssen. Besser ist es, die seit der Servlet-Spezifikation 3.0 existierenden Web-Fragmente zu verwenden.
Dazu legen wir neben der unveränderten normalen web.xml-Datei eine zweite web.xml-Datei für den Mock-Betrieb an, in der nur auf die zu startende Klasse verwiesen wird. Alle Einstellungen der Standard-web.xml bleiben erhalten, nur der Name der Anwendungsklasse wird somit überschrieben. Wenn wir die abgespeckte web.xml unter src/test/resources
ablegen, wird sie von Maven nach target/test-classes/web.xml
kopiert.
Und so schaut das web.xml
-Fragment aus:
version="1.0" encoding="UTF-8"?> WicketAppFilter org.apache.wicket.protocol.http.WicketFilter Starting MyApplication with mock objects applicationClassName com.company.MyMockApplication
Verteilung der Klassen und Ressourcen
Eine wesentliche Anforderung für den Mock-Betrieb ist die Trennung der Mock-Klassen vom übrigen Anwendungscode. Insbesondere dürfen weder Mock-Objekte noch die Mock-Infrastruktur Auswirkungen auf den produktiven Betrieb haben. Dies geschieht am besten, indem alle Dateien, die für den Mock-Betrieb benötigt werden, im Verzeichnisbaum src/test
abgelegt werden anstelle von src/main
.
Wenn die Anwendung (z.B. durch Maven) gebaut wird, werden nur die Bestandteile in src/main
einbezogen. Die Mock-Ressourcen können also nicht versehentlich mit eingepackt werden. Innerhalb der IDE hingegen lassen sich alle Ressourcen verwenden, die als Quellcode definiert sind. Hier lassen sich die Mock-Ressourcen unter src/test
problemlos verwenden.
Diese Aufteilung der verwendeten Klassen verdeutlicht das folgende Diagramm:
Fazit
Noch einmal kurz zusammgefasst: Für die Service-Schnittstelle haben wir ein Mock-Objekt geschrieben und eine Mock-Factory angelegt, die das Mock-Objekt erzeugt und für alle Referenzen der Schnittstelle zurück liefert. Die Mock-Factory wird über einen Mock-Injector immer dann aufgerufen, wenn Wicket eine @EJB-Annotation findet. Damit der Mock-Injector verwendet wird, muss er in der Wicket-Webanwendung explizit eingebaut werden. Dies geht am besten durch Vererbung der Webanwendungsklasse.
Schließlich bieten Jetty und die Servlet 3-Spezifikation die nötigen Mittel, um einen Web-Container zu starten, in dem die Webanwendung im Mock-Betrieb läuft. Eine Klasse mit einer main-Methode lässt sich problemlos aus jeder IDE starten – Funktionen wie das Debuggen und automatische Ersetzen von Code- oder Resourcenänderungen laufen reibungslos.
Am produktiv eingesetzten Wicket-Code ist keine Änderung nötig! Die Mock-Ressourcen können nicht versehentlich produktiv eingespielt werden.
Der Lohn der ganzen Mühe ist eine Umgebung speziell für die Bedürfnisse der Web-Entwickler. Die Entwicklung der Mock-Objekte führt zunächst zu einem deutlichen Mehraufwand. Dafür ist es dann möglich, ohne lange Build- und Deploymentzeiten Änderungen an der Web-Oberfläche umzusetzen und zu testen. Und wenn die Mock-Objekte einmal existieren, lassen sie sich auch sehr gut für Unit-Tests mit dem Wicket Tester verwenden. Dazu aber ein andernmal mehr.