Unit-Tests sind bei der Java-Entwicklung allgegenwärtig. Und das ist gut so. Aber beim Einsatz von Java EE tun sich viele Entwickler schwer damit, ihre Applikation automatisiert zu testen. Die "Magie" der verschiedenen Frameworks macht Tests umständlich oder scheinbar überflüssig. Wer will schon ein komplettes Deployment in einen Application Server durchführen, nur um einen einfachen Unit-Test auszuführen? Und CDI und JPA werden doch wohl korrekt in der Produktionsumgebung laufen, oder?
Dieser Vortrag zeigt, dass automatische Tests von Java-EE-Anwendungen weder umständlich, noch überflüssig sind. Im Gegenteil: Fast alle Komponenten einer üblichen Java-EE-Anwendung - wie Models, Repositorys, Services, Controller oder REST-Ressourcen - können ohne großen Programmieraufwand isoliert voneinander getestet werden. Und auch für den Test des korrekten Zusammenspiels der einzelnen Spezifikationen - wie JPA, JSF, CDI, JAX-RS oder Bean Validation - gibt es Frameworks, die dem Entwickler das Leben vereinfachen.
Von "einfachen" Unit-Tests mit JUnit 5, Spock und Mockito, über leichtgewichtige Integrationstests mit Derby und Jetty, bis hin zu kompletten End-to-End-Tests mit Arquillian, Selenium und WireMock werden alle Möglichkeiten für automatisierte Tests von Java-EE-Komponenten vorgestellt. Danach gibt es keine Ausrede mehr für weniger als 100% Code Coverage!
60. public class User {
@NotNull(message="User name may not be null")
private UserName userName;
@Size(min=1, message="Password too short")
private Password password;
80. @BeforeAll
public static void setupClass() {
EntityManager em = getEntityManager();
EntityTransaction t = em.getTransaction();
t.begin();
new TestDataCreator().createTestData(em);
t.commit();
82. sut = new JpaUserRepository(getEntityManager());
User expectedUser = TestData.validUser();
User actualUser = sut.findByUserName(...);
assertThat(actualUser, is(expectedUser)),
109. public class LoginPage {
static final By LOGIN_BUTTON =
By.id("loginForm:login");
static final By LOGGED_IN =
By.xpath("//h1[contains(text(),'Welcome')]");
public void login(String name, String password) {
driver.get(contextPath + "index.xhtml");
driver.findElement(USERNAME_FIELD).sendKeys(name);
driver.findElement(LOGIN_BUTTON).click();
Stefan ist Softwareentwickler und arbeitet mit Java.
Ganz besonders mag Stefan bei der Programmierung die Automatisierung von Tests.
In seinen aktuellen Projekten setzt er Java EE ein und tut sich etwas schwer mit den Tests.
Wie soll er die vielen Komponenten und insb. die „Magic“ von EE in Tests abdecken? Und ist das überhaupt sinnvoll?
Wie gut, dass es zu diesem Thema einen Vortrag auf der JavaLand gibt! Also lasst uns loslegen!
Ich würde gerne einen kleinen Überblick bekommen, wie weit ihr im Bezug auf automatisierte Tests seid.
Wer schreibt Unit-Tests?
Wer (davon) schreibt Integration-Tests?
Wer (davon) schreibt automatische Browser-/UI-Tests?
Vergleichen wir unser Ergebnis doch einmal mit einigen Umfragen.
Unit-Tests scheinen gängige Praxis zu sein.
Java Tools and Technologies Landscape 2014 (RebelLabs/ZeroTurnaround), 2164 Teilnehmer, http://pages.zeroturnaround.com/Java-Tools-Technologies.html
Survey Results: How Many Developers Write Unit Tests? (Hubstaff, 2014), mehrere hundert Teilnehmer, http://blog.hubstaff.com/survey-many-developers-write-unit-tests/
Und nun gehen wir einen Schritt weiter und fragen nach Browser-Tests.
Dort sieht die Situation schon etwas anders aus.
Java Tools and Technologies Landscape 2014 (RebelLabs/ZeroTurnaround), 2164 Teilnehmer, http://pages.zeroturnaround.com/Java-Tools-Technologies.html
Survey Results: How Many Developers Write Unit Tests? (Hubstaff, 2014), mehrere hundert Teilnehmer, http://blog.hubstaff.com/survey-many-developers-write-unit-tests/
Die verwendeten Tools laut einer Umfrage von 2014.
Java Tools and Technologies Landscape 2014 (RebelLabs/ZeroTurnaround), 2164 Teilnehmer, http://pages.zeroturnaround.com/Java-Tools-Technologies.html
Die Frage danach, ob die Entwickler gerne Tests schreiben, wurde größtenteils positiv beantwortet. Aber da geht noch mehr!
Survey Results: How Many Developers Write Unit Tests? (Hubstaff, 2014), mehrere hundert Teilnehmer, http://blog.hubstaff.com/survey-many-developers-write-unit-tests/
Was macht Unit-Tests schwierig?
Laut einer Studie wird auf der Liste häufiger Probleme beim Unit-Testing auf Platz 2 von 5 die Isolation der Unit genannt.
A Survey on Unit Testing Practices and Problems (Daka/Fraser, 2014), 225 Teilnehmer, http://ieeexplore.ieee.org/document/6982627/
Und genau darum soll es heute gehen: Wie können wir die einzelnen Komponenten einer Java-EE-Anwendung möglichst isoliert testen?
Also werden wir uns an den üblichen Komponenten einer Java-EE-Anwendung orientieren und sie Schritt für Schritt unter Test bringen.
Also lasst uns loslegen und schauen, wie wir eine Java-EE-7-Applikation komplett mit Tests abdecken können.
Aber bevor wir tiefer ins Thema einsteigen stelle ich mich noch kurz vor.
Ich habe einige Blogs, aber unter soa.rocks gibt es auch ab und an etwas über Java zu lesen. Keine Angst: Ich blogge (noch) nicht über Microservices! ;-)
Ihr könnt mir gerne auf Twitter folgen.
Und wenn ihr Lust habt, hört doch mal in meinen Podcast rein.
Dort gibt es über 100 Episoden.
Es gibt auch schon einige spannende Interviews. Zwei davon sogar mit Referenten der JavaLand vom letzten Jahr.
Ich komme aus Vechta, einer kleinen Stadt im Nordwesten Deutschlands mit ca. 30.000 Einwohnern.
Ich arbeite dort bei der ALTE OLDENBURGER Krankenversicherung AG als Softwareentwickler und -architekt.
Nebenberuflich bin ich noch Dozent für Programmierung und Software-Engineering an der PHWT.
Doch nun zurück zum Thema!
Kenntnisse von Java EE und den üblichen Pattern/Komponenten/Frameworks setze ich im Weiteren voraus.
Dies ist die Beispielapplikation, die uns durch alle Testphasen begleiten wird.
Sie ist sehr simpel aufgebaut, umfasst aber alle häufig verwendeten Technologien und Abstraktionen von Java EE.
Die Oberfläche ist mit Standardmitteln von JSF gebaut.
Der Datenbankzugriff läuft über JPA.
Für die Validierung wird Bean Validation genutzt.
Natürlich bietet die Anwendung eine REST-API für den Zugriff von außen an.
Und zuletzt werden noch per REST-Client externe Inhalte eingebunden.
Die Architektur ist in folgende Komponenten aufgeteilt.
Die Domain enthält die POJOs und die Geschäftslogik ohne Abhängigkeiten nach außen.
Das Repository ist für den Zugriff auf die Datenhaltung verantwortlich und wird mittels JPA realisiert.
Die Abhängigkeit geht in Richtung des Models.
Die Entitys in der Domain verwenden allerdings @Entity-Annotationen, um den Code nicht unnötig zu verkomplizieren.
Die Services stellen die fachlichen Schnittstellen nach außen dar. Sie verwenden ViewModels für den Datenaustausch mit den anderen Komponenten.
In der View werden JSF für die Oberflächen und JAX-RS für das Anbieten einer REST-API verwendet.
Dabei wird Model View Controller als internes Modell verwendet.
Zuletzt soll die Anwendung noch mit externen Systemen kommunizieren und verwendet dafür einen REST-Aufruf bzw. das Versenden von Mails.
Bevor wir loslegen noch ein paar allgemeine Hinweise.
Die gesamte Anwendung inkl. Testcode ist bei GitHub verfügbar.
Daher reduziere ich die Code-Beispiele stark auf die Kernaussagen.
Die vollständige Implementierung könnt ihr euch online anschauen.
Auch enthalten sind Dinge, die ich heute nicht zeigen kann, wie Mutation Testing und Property-based Testing.
Die Test-Pyramide ist sinnvoll: viele schnelle Unit-Tests (mit Mocking) und wenige langsame Integrationstests (mit Deployment) schreiben.
Der Fokus dieses Vortrags liegt darauf, wie wir statt Integrationstests Unit-Tests schreiben können, indem die zu testenden Komponenten voneinander isoliert werden.
Das Ziel ist daher, möglichst viele Tests im Java-SE-Kontext laufen zu lassen und nicht einen kompletten EE-Container dafür hochzufahren.
Dabei sollte jeweils nur das getestet werden, was die jeweilige Schicht selbst tut (z.B. bei REST-APIs HTTP-Codes erzeugen), und nicht z.B. Business-Logik durch die Oberfläche getestet werden.
Das Ziel ist dabei nicht (wie der Titel des Vortrags vermuten lässt), 100% Codeabdeckung zu erreichen.
Aufwand und Nutzen stehen dabei nämlich häufig in keinem sinnvollen Verhältnis.
Aus drei echten Projekten habe ich als Beispiel mal die letztendliche Codeabdeckung mitgebracht.
Beim ersten Projekt sieht die Coverage im Model z.B. recht gering aus.
Aber bei genauem Hinsehen fällt auf, dass dies eigentlich ausschließlich durch die fehlende Abdeckung der generierten (!) equals()-Methoden usw. zustandekommt.
Wie man damit umgeht, zeige ich später.
Beim letzten Projekt fällt die Testabdeckung der Kernkomponente – der Services – sehr hoch aus. Damit könnte ich gut leben, denn hier spielt die eigentliche Musik – die Geschäftslogik!
Zunächst wollen wir uns nun also anschauen, wie wir die Geschäftslogik unserer Anwendung testen können.
In der Architektur befinden wir uns nun im Bereich Model -> Domain und ViewModel.
So könnte eine Klasse aussehen, die wir testen wollen: Getter und Setter und fachliche Methoden.
Hierfür eignen sich „klassische“ Unit-Tests, die mittels Assertions die Rückgabewerte von Methodenaufrufen prüfen.
Wir verwenden dafür die neuste Version von JUnit, die einige hilfreiche Neuerungen mitbringt.
Zu Beginn zwei kurze Hinweise zur Benennung meiner Tests. Die Klassennamen enden meist auf „Should“, sodass sich sprechende Zusammenhänge aus Klassen- und Methodennamen ergeben: „User should -> be valid“.
Und die zu testende Klasse heißt in meinen Tests immer „sut“, um sie von evtl. notwendigen anderen Instanzvariablen abzugrenzen und eine einheitliche Benennung in allen Tests zu haben.
Je nach Laune verwende ich für die Assertions AssertJ, um etwas sprechendere Tests zu erzeugen.
Beispiel: PasswordShould
Dumme POJOs und Beans (wie z.B. die ViewModels) lassen sich automatisiert mittels Reflection testen, auch wenn der Mehrwert fragwürdig ist.
Beispiel: UserShould
Mit den Bean Matchers bekommt man dann auch tatsächlich 100% Code Coverage hin!
BeanValidation lässt sich recht einfach testen.
Es ist lediglich eine Implementierung des Standards (z.B. Hibernate Validator) nötig und der Validator liefert alle Validierungsergebnisse für einfache Assertions.
Beispiel: UserShould
Damit befinden wir uns nun schon im nächsten Bereich der Architektur: Service und ViewModel.
Und wir gehen nun in Richtung Test von Abhängigkeiten und schauen uns dafür einen Service an.
So könnte eine Klasse aussehen, die externen Input benötigt bzw. externe Aufrufe tätigt.
Beide Varianten werden unterschiedlich getestet, also sollten wir sie trennen.
Beginnen wir mit den externen Inputs.
Wir testen sie mit Stub-Objekten, die das Verhalten der Abhängigkeiten simulieren und harte Werte zurückliefern. Dafür hat sich das Framework Mockito etabliert.
Beispiel: UserServiceShould
Am Test ändert sich nichts: Inputs werden gestubbt, Tests verwenden weiterhin normale Assertions.
Dabei ist irrelevant, wie diese Abhängigkeit erzeugt wird, also z.B. als Konstruktorparameter oder per Dependency Injection.
Attribute, die über das häufig genutzte @Inject gefüllt werden, können z.B. durch setzen des entsprechenden Attributs auf package-private direkt im Test zugewiesen werden.
Das bricht die Kapselung zwar ein wenig auf, erleichtert den Test aber deutlich. Und es ist kein DI-Framework nötig.
Es ist sogar möglich, statische Methoden zu stubben. Das Framework PowerMock macht es möglich.
Beispiel: UserServiceAlsoShould
Nun schauen wir uns an, wie wir Seiteneffekte testen, die von der zu testenden Klasse ausgelöst werden.
Dafür verwenden wir Mocks, die sich „merken“ können, wie mit ihnen interagiert wurde.
Die Tests verändern sich: Statt Assertions gegen Rückgabewerte werden nun Vergleiche mit dem gewünschten Kommunikationsverhalten durchgeführt (verify).
Beispiel: UserServiceShould
Da Services häufig „nur“ Repositorys und andere Services orchestrieren, kann ihr Verhalten gut mittels Mocks getestet werden.
Viele Klassen nutzen Logger (z.B. von SLF4J), die auch im Test vorhanden sein müssen und gestubbt werden können.
Falls es zum Use-Case dazugehört, könnte sogar geprüft werden, ob Logaufrufe stattgefunden haben.
Beispiel: SessionResourceShould
Als nächstes schauen wir uns die Tests für die Persistenzschicht an.
Wir befinden uns nun im Bereich der Persistence, für die JPA als Technologie eingesetzt wird.
So könnte ein typisches Repository aussehen, das letztlich nur einen Wrapper um den Entity Manager darstellt.
Für den Großteil der Funktionalität muss lediglich der EntityManager gemockt werden.
Das kann durch Setzen der Instanzvariable auf package-private analog zum Service einfach erreicht werden.
Beispiel: JpaUserRepositoryShould
Das Setup wird nun etwas komplexer, da teilweise auch die Returnwerte des Entity Managers erneut gemockt werden müssen.
Aber mit sprechenden Hilfsmethoden im Testcase kann man trotzdem gut lesbare Tests schreiben.
Das hier ist ein komplettes Beispiel für einen AAA-Test mit gemocktem Entity Manager.
Durch das Mocken des Entity Managers können wir leider nicht die korrekte Konfiguration (Annotationen für Ids, Beziehungen, Serializable, Custom Converter usw.) der Entity-Klassen testen.
Für den Test der „JPA-Magic“ nutzen wir daher eine echte (aber leichtgewichtige) Datenbank, die einen Zugriff auf Testdaten ermöglicht.
Dafür wird Derby als Datenbank und EclipseLink als JPA-Referenzimplementierung verwendet.
Man kann dafür entweder eine eigene Persistence Unit in der persistence.xml in main anlegen oder eine zweite persistence.xml in test hinzufügen. Ich tendiere zu Letzterem, damit kein Test-Artefakt im Produktionscode liegt.
Die persistence.xml reicht aus, um zur Testlaufzeit eine funktionsfähige Datenbank vorzufinden, die direkt ohne weiteres Setup genutzt werden kann.
Lediglich der Inhalt der Testdatenbank ist noch anzulegen. Dafür ist ein manueller Aufruf des Entity-Managers mit eigener Transaktionssteuerung zu Beginn des Tests nötig, da wir uns noch nicht im EE-Container befinden.
Die Testdaten können nun ganz normal mit dem Entity Manager erzeugt werden.
Ein Aufräumen/Teardown ist nicht nötig, da die Datenbank mittels der entsprechenden EclipseLink-Option automatisch beim Starten gelöscht und neu angelegt wird.
Nun kann ganz „normal“ getestet werden. Das Repository muss lediglich den Entity Manager gesetzt bekommen, da es ihn nicht automatisch injiziert bekommt.
Beispiel: JpaUserRepositoryIntegrationTest
Die Controller der View-Komponente lassen sich recht gut isoliert testen. Dazu ist weder JSF noch ein EE-Container notwendig.
Wir befinden uns in der Architektur nun im Frontend und schauen uns im Detail die Controller an.
Ein Controller kommuniziert laut Architektur mit dem Service im Model, um die gewünschten Use-Cases umzusetzen. Dabei verwenden beide die ViewModels als Parameter.
Außerdem wird häufig der FacesContext genutzt, um z.B. Meldungen anzuzeigen.
Hier ist das Anzeigen einer solchen Meldung zu sehen.
Man könnte nun also auf die Idee kommen, den FacesContext wie gewohnt als package-private Instanzvariable anzulegen und zu mocken.
Der Test ist nun nur noch ein simpler String-Vergleich für die Zielseiten und ein verify() für die Messages.
Beispiel: LoginControllerShould
Die angebotene REST-Schnittstelle wird als nächstes getestet.
Die REST-API zählt auch zum Frontend der Anwendung. Wie man sie testen kann, wird als nächstes behandelt.
Der Aufbau ist analog zum Controller: Eine Ressource nutzt den Service im Model zur Umsetzung der Use-Cases.
Der Service kann wieder gemockt werden und die Tests (z.B. der korrekten Return-Codes) verlaufen analog zum Controller.
Beispiel: SessionResourceShould
Um allerdings den korrekten Aufbau der URLs und die Annahme/Rückgabe der richtigen Content-Types zu testen, müssen wir die REST-API in einen Servlet-Container deployen.
Dazu verwenden wir den leichtgewichtigen Container Jetty, der mit wenigen Zeilen Code eingerichtet und gestartet werden kann.
Für den Test kann nun einfach mit Jersey ein REST-Client entwickelt werden, mit dem dann normale Assertions durchgeführt werden können.
Für ein wenig mehr Komfort beim Testen der REST-Services kann RESTassured eingesetzt werden, das eine Fluent-API anbietet.
Dann muss nicht manuell mit Jersey ein eigener REST-Client erstellt werden.
Beispiel: SessionResourceIntegrationTest
Die gesamte Anwendung als Ganzes kann inkl. eines Deployments in einen Container automatisiert getestet werden.
Um das Zusammenspiel der gesamten Anwendung zu testen, wird diese in einen EE-Container deployt. Hier kann sie unter realistischen Bedingungen mit allen nötigen EE-Frameworks getestet werden.
Alle Komponenten sollen nun im Zusammenspiel getestet werden. Insbesondere die Dependency Injection und die Verbindung zur Datenbank stehen hierbei im Fokus.
Arquillian ist ein Integration-Test-Framework für Java-EE-Anwendungen, das die gesamte Applikation in einen echten Application Server deployt.
Arquillian unterstützt eine Vielzahl an Zielcontainern.
arquillian.xml -> Target für Deployment definieren
Chameleon lädt Dependencies automatisch beim Ausführen des Tests herunter
Artefakt der Anwendung wird erstellt
Zeigt auf In-Memory-DB des Zielsystems mit create-drop
Mittels Observer werden automatisch die Testdaten in die Datenbank geschrieben
Anwendung wird deployt -> generierte URL als WebTarget injiziert
Arquillian führt die Tests direkt im Container aus bzw. ermöglicht Zugriff von außen über WebTarget z.B. für RESTassured
Da wir nun die Anwendung zur Testzeit automatisiert deployen können, kann auch die Weboberfläche mit Tests versehen werden.
Der letzte noch zu testende Teil der Frontend-Architektur sind die JSF-Seiten. Da hierfür ein Deployment in einen EE-Container notwendig ist, bauen die Tests immer auf diesem auf.
Hier geht es nun nicht mehr um automatische Tests der Komponenten, sondern um echte End-to-End-Tests der Oberfläche, also die Simulation der Benutzereingaben.
Der de facto Standard für Weboberflächentests ist das Framework Selenium.
Es kann verschiedene Browser simulieren bzw. fernsteuern.
Und einer dieser Browser ist PhantomJS, ein „headless“ JavaScript-Browser.
Das Framework Drone verbindet Selenium mit Arquillian und Graphene bietet einige nette Features, die das Schreiben von Tests einfacher macht.
Drone: Wrapper um WebDriver, Lifecycle-Management für Browser, mehrere Browser pro Test
Graphene: Page-Abstraktion, AJAX-Unterstützung, Waiting-API
Die zu testenden Webseiten können in einzelnen Klassen abgebildet werden, die mittels CSS-Selektoren, XPath oder anderen Zugriffsmethoden automatisiert gesteuert werden können.
Beispiel: LoginPage
Unsere eigene Anwendung steht nun komplett unter Tests. Aber die Abhängigkeiten von fremden Services gilt es noch mit Tests abzudecken.
Der letzte Teil unserer Architektur sind die externen Services, die von unserer Anwendung über Standardprotokolle wie HTTP oder SMTP aufgerufen werden.
So könnte ein interner REST-Client aussehen, der eine externe Ressource abfragt.
Die Anfrage an den Service sollte von der Verarbeitung der Ergebnisse getrennt werden. Dann kann ersterer gemockt werden, während die Verarbeitung normal mit Unit-Tests abgedeckt wird.
Beispiel: ArticleService
Die Verarbeitung des JSON-Strings, der vom Fremdsystem geliefert wird, kann ganz einfach getestet werden.
Beispiel: ArticleServiceShould
Der Hostname des Zielsystems muss konfigurierbar sein, sodass im Test localhost verwendet werden kann.
Das geht der Einfachheit halber wieder gut mit package-private Attributen.
Für den Test gegen einen „echten“ REST-Endpunkt kann nun Wiremock verwendet werden.
Es wird zur Laufzeit ein Webserver gestartet, der REST-Anfragen entgegennimmt.
Beispiel: ArticleServiceIntegrationTest
Für Tests, die mehr als eine REST-Schnittstelle ansprechen, gibt es noch Citrus.
Das Framework kann alle erdenklichen Schnittstellen mocken, z.B. SOAP, REST, JMS, FTP, SSH.
Damit komme ich zum Ende meines Vortrags.
Java bietet eine Vielzahl an ausgereiften Frameworks rund ums Testen bis hin zum automatischen Deployment in einen Application Server.
Stefan ist durch die ganzen Möglichkeiten jetzt allerdings mehr verwirrt als vorher.
Er wollte doch nur ein paar Unit Tests schreiben…
…und nicht gleich ein ganzes Sammelsurium an Frameworks lernen und kombinieren.
Aber man muss ja nicht gleich mit allen Teilen starten, sondern kann langsam anfangen.
Macht euch Gedanken über die Isolation eurer Units und legt mit dem gewohnten JUnit und Mockito los.
Und Schritt für Schritt fügt ihr für alle Komponenten die passenden Frameworks hinzu.
Der Aufwand für automatische Tests lohnt sich immer! Also fangt am besten gleich morgen damit an!
Vielen Dank!
Wie gesagt: Das komplette Projekt findet ihr auf GitHub.