Handwerker - Testing

Next Generation Application Development: Java Enterprise Testing

Marcel Birkner

In diesem Teil der Serie stellen wir verschiedene Möglichkeiten vor, wie komplexe Enterprise-Applikationen getestet werden können. Wir werden viele Beispiele verwenden, um die unterschiedlichen Testansätze zu erklären.

Seit der Einführung von Java EE ist das Testen von Enterprise-Applikationen schwieriger geworden. Ein wesentlicher Grund liegt darin, dass Komponenten in einer Enterprise-Architektur selten in sich abgeschlossen sind, sondern mit vielen anderen Komponenten des Systems zusammenspielen. Java-EE-6-Container wie Glassfish v3 oder JBoss AS6 sind zum Beispiel für das Dependency Management und die Persistenz zur Datenbank (JPA 2.0) verantwortlich. Das Zusammenspiel der Geschäftslogik mit anderen Komponenten wird in Enterprise-Applikationen immer wichtiger, ähnlich wie die Implementierung der Geschäftslogik selbst. Vor allem deklarative Beschreibungen, wie Dependency Injection und Transaktionskontrollen, müssen daraufhin getestet werden, ob sie die gewünschten Aufgaben erfüllen. Unit-Tests und Mock-Tests können nur einen begrenzten Teil dieser Überprüfungen übernehmen. Integrationstests spielen daher eine immer wichtigere Rolle.

Artikelserie
  • Teil 1: Rookie – praktische Einführung
  • Teil 2: Handwerker – Testing
  • Teil 3: Experte – fortgeschrittene Konzepte
  • Teil 4: Guru – Framework-Integration

In diesem Artikel werden wir anhand von verschiedenen Szenarien zeigen, wie Enterprise-Applikationen getestet werden können. Dazu verwenden wir als Beispiel einen Web Service mit Anbindung an eine Datenbank. Dieser besitzt mehrere Methoden zum Kundenmanagement. Natürlich möchten wir diese soweit möglich 100-prozentig mit Tests abdecken. In den Beispielen verwenden wir CDI (JSR-299), JAX-WS und JPA 2.0 und werden auf verschiede Möglichkeiten des Testens eingehen. Alle Beispiele befinden sich als Maven-Projekt auf GitHub und können unter [1] heruntergeladen werden.

Unit Testing am Beispiel: Testing injizierte Bean

Zuerst betrachten wir den Test in Listing 1.1. Die Bean CustomerCreator, die wir testen wollen, enthält eine Bean UniqueIdentifierUtil, die mit @Inject injiziert wird. Diese Utility Bean ist für die Generierung der customerId verantwortlich. Eine Möglichkeit diese Bean ohne großen Aufwand zu testen, ist es, ein Mocking-Framework wie Mockito, JMock oder EasyMock zu verwenden. Wir haben uns bei unseren Projekten für Mockito entschlossen, was sich bisher sehr gut bewährt hat [2]. Damit die Bean gemockt werden kann, müssen wir sie für den Unit-Test um einen Konstruktor oder eine Setter-Methode erweitern, der die UniqueIdentifierUtil Bean enthält. Dadurch können wir für Unit-Tests die Utility Bean durch einen Mock austauschen, der den Test nachvollziehbar und kontrollierbar macht. Diese Erweiterung des Quellcodes stellt nur einen kleinen Nachteil dar, der leicht durch die Vorteile der besseren Testabdeckung aufgewogen wird. Es ist wesentlich wichtiger, den Quellcode testbar zu halten, als auf Tests zu verzichten. Das scheint auch mehrheitlich die Meinung der Entwickler-Community zu sein [3].

In Listing 1.2 ist zu sehen, wie wir der in der @BeforeMethod annotierten Methode unser Mock-Objekt durch Mockito initialisieren. In der mit @Test annotierten Methode definieren wir zuerst mit dem Mockito-API was wir beim Aufruf der Utility-Methode erwarten. Danach folgt der eigentliche Test.

Listing 1.1
 @Inject Bean testen
public class CustomerCreator {

  @Inject UniqueIdentifierUtil util;

  // For Unit Tests
  public CustomerCreator(UniqueIdentifierUtil util) {
    this.util = util;
  }
  
  public Customer createNewCustomer(String firstName, String lastName ) {
    Customer c = new Customer();
    c.setCustomerId( util.getNextCustomerId() );
    c.setFirstName(firstName);
    c.setLastName(lastName);
    return c;
  }
}
Beispiel: Mocking Persistence
 @Inject mit Mockito testen
public class CustomerCreatorTest {
  
  @Mock UniqueIdentifierUtil util;

  CustomerCreator underTest;

  int customerId = 42;
  String firstName = "John";
  String lastName = "Doe";
  
  @BeforeMethod
  public void setup() {
    MockitoAnnotations.initMocks(this);
    underTest = new CustomerCreator( util );
  }
  
  @Test
  public void createNewCustomerShouldReturnNewlyCreatedCustomer () {
    when( util.getNextCustomerId() ).thenReturn( customerId );
    Customer c = underTest.createNewCustomer(firstName, lastName);
    assertEquals(customerId, c.getCustomerId() );
    assertEquals(firstName, c.getFirstName() );
    assertEquals(lastName, c.getLastName() );
  }
}

Auf die gleiche Art und Weise wie in Listing 1.2 kann auch der EntityManager unserer Applikation gemockt werden. Alle Methodenaufrufe an ihn können so mit vordefinierten Antworten gemockt werden. Listing 2.1 und 2.2 zeigen dies anhand eines Beispiels. Es entsteht ein großer Vorteil, indem wir die komplette Persistenzschicht aus den Unit-Tests entfernen bzw. mocken können und damit reproduzierbare Tests erhalten. Die Tests sind sehr performant und können vom Entwickler häufiger ausgeführt werden, was wiederum die Turnaround-Zeit bei der Entwicklung verbessert.

Listing 2.1 JPA – Persistence Service
public class PersistenceServiceTransaction {

  @Inject EntityManager em;

  // For Unit Test
  public PersistenceServiceTransaction(EntityManager em) {
    this.em = em;
  }

  public Customer findCustomerByCustomerId(int customerId) {
    Customer customer = em.find(Customer.class, customerId);
    return customer;
  }
}
Listing 2.2: JPA – Persistence Service Test mit Mockito
public class PersistenceServiceTransactionTest {

  @Mock EntityManager em;

  PersistenceServiceTransaction underTest;

  int cid = 42;
  
  @BeforeMethod
  public void setup() {
    MockitoAnnotations.initMocks(this);
    underTest = new PersistenceServiceTransaction( em );
  }

  @Test
  public void findCustomerByNonExistingCustomerIdShouldReturnNull() {
    Customer c = underTest.findCustomerByCustomerId( cid );
    assertNull( c );
  }
  
  @Test
  public void findCustomerByValidCustomerIdShouldReturnCustomer() {
    when( em.find( Customer.class, cid) ).thenReturn( getStub( cid ) );

    Customer c = underTest.findCustomerByCustomerId( cid );
    assertEquals(cid, c.getCustomerId() );
    .
  }

  private Customer getStub( int customerId ) {
    Customer customer = new Customer();
    customer.setCustomerId( customerId );
    .
    return customer;
  }

}
Integrationstests

Die vorherigen Beispiele verdeutlichen wie Mocking-Frameworks Entwicklern helfen können, Unit-Tests für Container gemanagte Komponenten zu schreiben. Was wir mit Mocking-Frameworks jedoch nicht testen können, ist das Zusammenspiel verschiedener Komponenten im Container, dem Gesamtsystem, in dem sie laufen. Hierfür sind Integrationstests zwingend notwendig, die uns die notwendige Sicherheit geben sollen, dass alle Containerkomponenten ihre Aufgabe erfüllen. Bisher gab es keine Frameworks die den Entwickler bei den Integrationstests unterstützen konnten. Genau an dieser Stelle möchte das JBoss Projekt Arquillian die Arbeit der Entwickler erleichtern [4]. Arquillian erlaubt es, die Testklasse, zusammen mit dem Produktionscode, in einem Container laufen zu lassen. Dabei ist das Framework verantwortlich für den kompletten Containerlebenszyklus (start/stop), die Paketierung der Testklassen, der abhängigen Klassen, der Resource-Dateien und für das Deployment des Archivs, das getestet werden soll (deploy/undeploy). Arquillian integriert sich mit JUnit 4 und TestNG 5 und möchte dem Entwickler so die Komplexität der Integrationstestumgebung abnehmen. Es kann auf diese Weise mit allen existierenden IDEs verwendet werden und der Entwickler braucht keine weiteren Tools erlernen. An dieser Stelle sei der Artikel von Michael Schütz und Alphonse Bendt im Java Magazin 1.2011 genannt. Dieser beschreibt sehr ausführlich die Grundlagen von Arquillian.

Bevor wir später auf wesentliche Details des Frameworks eingehen, werden wir uns seinen Einsatz anhand von ein paar Quellcodebeispielen anschauen. Dafür erweitern wir die vorherigen Beispiele und stellen mithilfe der folgenden Listings 3.1 – 3.5 einen kompletten Integrationstest vor. Dieser wird die Methoden des Web Service aufrufen und Daten via JPA 2.0 aus einer Datenbank lesen und schreiben. Für Testzwecke verwenden wir HSQL als dateibasierte Datenbank. Das ermöglicht es uns, performante Integrationstests zu schreiben. Die einzelnen Beans verwenden CDI Annotationen und Qualifier für die Dependency Injection. Die folgende Auflistung beschreibt alle Klassen in diesem Beispiel und deren Aufgaben:

  • CustomerServiceImpl ist die Implementierung des Web Services und stellt die createNewCustomer– und findCustomerByCustomerId-Methoden zur Verfügung. Diese Methoden wollen wir im Integrationstest aufrufen. Die JAX-WS-Annotationen wurden aus Platzgründen entfernt, sind aber auf GitHub vorhanden [1].
  • CustomerServiceContext bekommt die EntityManagerFactory vom Container injiziert und stellt den EntityManager zur Verfügung.
  • TestPersistence wird nur für den Integrationstest verwendet und erzeugt anhand der @Produces-Annotation die EntityManagerFactory, die im CustomerServiceContext verwendet wird. Diese Bean ersetzt den Produktionscode der CustomerServicePersistence-Bean. Die Details zur Persistence Unit werden in der persistence.xml beschrieben [1]. Aufgrund eines Bugs in Glassfish v3 konnten wir den Entity-Manager nicht direkt in den Service injizieren.
  • CustomerServiceTransaction ist die Implementierung für die Persistenzschicht. Der Entity-Manager wird im Konstruktor der Klasse mit übergeben.
  • CustomerServiceImplTest ist der Integrationstest. Da wir für dieses Beispiel TestNG verwenden, muss der Test von der Klasse org.jboss.arquillian.testng.Arquillian ableiten. Für JUnit würde die Klasse mit @RunWith(Arquillian.class) annotiert werden [5]
    . @Deployment markiert die Methode, die das Archiv erzeugt, des für das Deployment im Container verwendet wird. Innerhalb dieser Methode wird ShrinkWrap verwendet, um programmatisch ein Deployment-Archiv zu erzeugen. In diesem Fall enthält das Archiv eine leere beans.xml, um CDI zu aktivieren, und eine Reihe von Klassen, die vom Container für den Integrationstest benötigt werden. ShrinkWrap sammelt alle benötigten Klassen automatisch aus dem Klassenpfad ein und erzeugt WAR, JAR oder EAR Archive, welche direkt in den gewünschten Container deployed werden [6]. Durch den Aufruf dieses Tests startet Arquillian den Container, generiert mithilfe von ShrinkWrap das passende Deployment-Archiv, deployt es, führt alle Testfälle der Reihe nach aus, entfernt das Archiv (undeploy) und stoppt den Container. Dies alles geschieht für den Entwickler transparent.
Listing 3.1: CustomerService-Implementierung
 Service Test mit Mockito
public class CustomerServiceImpl {

  @Inject CustomerServiceContext context;

  public boolean createNewCustomer( Customer customer ) {
return getTransaction().createNewCustomer( customer ) ;
  }
  
  public Customer findCustomerByCustomerId( int customerId ) {
return getTransaction().findCustomerByCustomerId( customerId );
  }

  private CustomerServiceTransaction getTransaction() {
EntityManager em = context.createEntityManager();
    return new CustomerServiceTransaction( em );
}  
}
Listing 3.2: CustomerServiceContext
@Singleton
public class CustomerServiceContext implements Serializable {
private static final long serialVersionUID = 1L;

      @Inject @CustomerService EntityManagerFactory factory;

public EntityManager createEntityManager() {
return factory.createEntityManager();
}
}
Listing 3.3: Test Persistence
@Singleton
public class TestPersistence implements Serializable  {
  private static final long serialVersionUID = 1L;
  
  EntityManagerFactory factory=Persistence.createEntityManagerFactory("test");
  
  @Produces @CustomerService
  public EntityManagerFactory getEntityManagerFactory() {
    return factory;
  }
}
Listing 3.4: JPA – CustomerServiceTransaction-Implementierung
public class CustomerServiceTransaction {

  private EntityManager em = null;

  public CustomerServiceTransaction(EntityManager em) {
    this.em = em;
  }

  public boolean createNewCustomer( Customer customer ) {
    em.getTransaction().begin();
    em.persist( customer );
    em.getTransaction().commit();
    return true;
  }
  
  public Customer findCustomerByCustomerId( int customerId ) {
    return em.find( Customer.class, customerId );
  } 
}
Listing 3.5: CustomerService Test mit Arquillian
public class CustomerServiceImplTest extends Arquillian {
  
  @Deployment
public static Archive> createTestArchive() throws IllegalArgumentException, IOException {
    JavaArchive archive = 
Archives.create("test.jar", JavaArchive.class).
addManifestResource( new ByteArrayAsset( new byte[0]), ArchivePaths.create("beans.xml") ).
    addClasses( 
CustomerServiceImpl.class, 
CustomerServiceContext.class, 
TestPersistence.class );
    return archive;
  }

  @Inject  CustomerServiceImpl underTest;
  
@Inject @CustomerService EntityManagerFactory factory;

  int customerId = 42;
  String firstName = "John";
  String lastName = "Doe";

  @Test
  public void createCustomerShouldReturnSuccessful() {
    Customer c = new Customer();
    c.setCustomerId( 100 );
    c.setFirstName( firstName );
    c.setLastName( lastName );
    assertEquals(true, underTest.createNewCustomer( c ) );
  }

  @Test
  public void findCustomerByCustomerIdShouldReturnNewlyCreatedCustomer() {
    Customer c = new Customer();
    int cid = 100;
    c.setCustomerId( cid );
    c.setFirstName( firstName );
    c.setLastName( lastName );
    underTest.createNewCustomer( c );
Customer foundCustomer = underTest.findCustomerByCustomerId( cid );
    assertEquals(cid, foundCustomer.getCustomerId() );
  }
.
}
Features von Arquillian

Arquillian kombiniert Unit-Test-Frameworks, ShrinkWrap und viele unterstützte Container, um dem Entwickler eine einfache, flexible und erweiterbare Integrationstestumgebung zur Verfügung zu stellen.
Abbildung 1

zeigt das Zusammenspiel der vielen Komponenten des Frameworks [5]
. Das Framework kann mit JUnit und TestNG ausgeführt werden und abstrahiert den Container-Lifecycle. Arquillian unterstützt das Debugging für alle bekannten IDEs. Vor dem Test wird der Container gestartet, danach das Archiv (z.B. WAR, JAR, EAR) deployed, der Test ausgeführt, alle Testergebnisse aufgezeichnet, das Archiv undeployed und zum Schluss der Container gestoppt. Alle Vorgänge bleiben dabei für den Entwickler transparent. Beim Ausführen der Tests werden alle Resourcen, Managed Beans und EJBs automatisch injiziert. Zudem unterstützt Arquillian alle bekannten Java-Enterprise-6-Container im Embedded und Remote Modus, sowie Servlet-Container, z.B. JBoss AS, GlassFish, Jetty, Tomcat, OpenEJB.

Abb. 1: Arquillian Architektur [3]

Remote-Container werden in einer eigenen JVM ausgeführt und Tests werden über ein Remote-Protokoll (JNDI bei JBoss) verwaltet. Remote-Container lassen sich nutzen, wenn ein Continuous-Integration-Server (Jenkins/Hudson) Tests auf verschiedenen Containern ausführen soll.

Embedded-Container werden in der gleichen JVM wie der Test-Runner ausgeführt und der Lifecycle wird direkt von Arquillian gemanaged. Dies empfiehlt sich für lokale Tests der Entwickler.

Managed-Container sind vergleichbar mit Remote-Containern, jedoch verwaltet Arquillian das Starten/Stoppen des Containers selbst. Die verschiedenen Container können als Maven Profiles konfiguriert und je nach Bedarf beim Ausführen der Tests ausgetauscht werden. Für weitere Details zu diesem Aspekt empfiehlt sich die offizielle Dokumentation, Kapitel 9 Complete Container Reference [5]
.

Extensions für Arquillian

Zusätzlich zu den beschriebenen Features bietet Arquillian noch weitere nützliche Erweiterungen. Bei der ersten handelt es sich um eine Extension für einfache Performance-Tests. Mit der @Performance-Annotation kann die maximale Ausführdauer (in ms) des Methodenaufrufs an jeder Testmethode deklariert werden (siehe Listing 4.1 und 4.2). Eine weitere Annotation @PerformanceTest an der Testklasse sorgt dafür, dass die Laufzeit jedes Tests gespeichert wird und mit der Laufzeit des nächsten Aufrufs verglichen wird. Wird der angegebene Threshold überschritten, schlägt auch dieser Test fehl.

Listing 4.1: @Performance
  @Test
@Performance( time=50 )
  public void createCustomerShouldReturnSuccessful() {
    Customer c = new Customer();
    c.setCustomerId( 100 );
    c.setFirstName( firstName );
    c.setLastName( lastName );
    assertEquals(true, underTest.createNewCustomer( c ) );
  }
Listing 4.2: Maven Dependency für Performance Extension
 @Inject mit Mockito testen
public class CustomerCreatorTest {
  
  @Mock UniqueIdentifierUtil util;

  CustomerCreator underTest;

  int customerId = 42;
  String firstName = "John";
  String lastName = "Doe";
  
  @BeforeMethod
  public void setup() {
    MockitoAnnotations.initMocks(this);
    underTest = new CustomerCreator( util );
  }
  
  @Test
  public void createNewCustomerShouldReturnNewlyCreatedCustomer () {
    when( util.getNextCustomerId() ).thenReturn( customerId );
    Customer c = underTest.createNewCustomer(firstName, lastName);
    assertEquals(customerId, c.getCustomerId() );
    assertEquals(firstName, c.getFirstName() );
    assertEquals(lastName, c.getLastName() );
  }
}

Zudem bietet Arquillian eine Erweiterung für JSFUnit, die es erlaubt einzelne JSF-Seiten separat zu testen. Dazu ist JSFUnit 1.3.0.Final notwendig, sowie ein Java-EE-6-Container [5]
. Diese Erweiterungen werden mit der Maven Dependency, wie in Listing 4.2 beschrieben, aktiviert.

Zusammenfassung

Die hier vorgestellten Frameworks bieten Entwicklern die Möglichkeit eine sehr hohe Testabdeckung für ihre Enterprise-Applikationen zu erreichen. Nahe an die 100-prozentige Code Coverage heranzukommen, wie in unserem Beispielprojekt ( Abb. 2), ist dabei nicht unrealistisch. Mockito hat sich als Mocking-Framework bereits über viele Releases hinweg bewährt. Arquillian ist auf einem sehr guten Weg die gleiche Produktionsreife wie andere Testing-Frameworks zu erreichen. Da es in absehbarer Zeit keinen weiteren Konkurrenten für Arquillian im Bereich der Integrationstest-Frameworks für Java-EE-Applikationen geben wird, wird Arquillian in der Zukunft sicherlich zum Bestandteil vieler Java-Enterprise-Projekte gehören. In unseren Projekten haben wir mit Arquillian sehr gute Erfahrungen gemacht, auch wenn es hier und da noch ein paar Bugs gibt. Insgesamt ist die Dokumentation sehr gut und die Community um Arquillian sehr aktiv.

Abb. 2: Test Coverage des Beispielquellcodes [1]
Marcel Birkner studierte an der Hochschule Bonn-Rhein-Sieg und der York University in Toronto mit dem Schwerpunkt Softwarearchitektur und Design. Er arbeitet für die M-net Telekommunikations GmbH als Softwareentwickler und beschäftigt sich mit der Einführung von Java EE 6 für Teile der IT-Landschaft.
Geschrieben von
Marcel Birkner
Kommentare

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht.