Services – TDD mit Eclipse, OSGi, REST, Jetty

Freitag, 31. Oktober 2014 von  
unter Fachartikel Architektur

Die Motivation

In diesem Blog-Eintrag geht es um das Thema TDD von RESTful OSGi Applikationen mit Eclipse. Hier speziell darum, unter Verwendung des „Eclipse RESTfuse“ – Plugins und des „Tycho-Eclipse Build“ – Plugins einen mittels OSGi-JAX-RS Connector veröffentlichten REST Services innerhalb einer OSGi Instanz zu testen.

Über „Eclipse RCP – OSGi, Equinox und SOA“ gibt es bereits einen Blog-Eintrag in diesem Blog hier und weitere Eclipse RCP Blog-Einträge hier, hier und hier. Über das TDD von RESTful WebServices mittels Swing Test-Client gibt es hier ebenfalls einen Blog-Eintrag. Bekanntlich bleibt gegenüber der angestrebten Plattform- und Implementierungs-Sprachunabhängigkeit von WebServices, OSGi auf die Plattform der Java Virtual Machine (JVM) fixiert und ist nicht komplett sprachunabhängig, jedoch können alle JVM-basierten Sprachen wie Java, Scala, etc. für die Erstellung der Services verwendet werden. Nun zur der Frage, wie ein Integrations-Test für einen mittels OSGi-JAX-RS-Connector erstellten REST-Service in einem Eclipse Tycho Build ausgeführt werden kann.

Die Grundlagen

OSGi-JAX-RS-Connector: ist eine Library von Holger Staudacher. Der Connector ermöglicht es, Ressourcen leicht zu veröffentlichen, um mit @Path annotierte Typen als OSGi Services zu registrieren.

Tycho: ist ein Set von Maven Plugins und Erweiterungen, für den Maven-Build von Eclipse Plugins und OSGi Bundles. Eclipse Plugins und OSGi Bundles haben ihre eigenen Metadaten für Dependencies, Sourcecode Verzeichnisse, Locations, etc., die üblicherweise in der Maven pom.xml zu finden sind. Tycho verwendet den Maven-bezogenen, Manifest-Ansatz für den Build von Eclipse Plugins, Features, Update Sites, RCP Applikationen und OSGi Bundles.

Tycho verwendet native Metadaten für Eclipse Plugins und OSGi Bundles und die pom.xml zur Konfiguration und um den Build auszuführen. Tycho unterstützt Bundles, Fragmente, Features, Update Site Projects und RCP Applicationen. Mit Tycho können auch JUnit Test Plugins unter Verwendung der OSGi Runtime gestartet werden, weiterhin gibt es Unterstützung für Maven Artifact Repositories, um Build Ergebnisse geshared mehrfach zu verwenden. Mit Tycho Plugins gibt es neue Packaging Typen und die entsprechenden Lifecycle Bindings, damit Maven, OSGi und Eclipse während eines Maven Builds die Metadaten verwenden können.

Restriktionen zur Package-Sichtbarkeit werden durch die Verwendung des OSGi-erkennenden JDT-basierten Compiler Plugins beachtet. Tycho verwendet dann die OSGi Metadaten und OSGi Rules um die Projekt Dependencies dynamisch aufzulösen und injectet diese dann zum Build-Zeitpunkt in das Maven Projekt Modell. Tycho unterstützt alle Attribute die vom Eclipse OSGi Resolver erkannt werden (Require-Bundle, Import-Package, Eclipse-GenericRequire, etc) und verwendet geeignete Classpath Regeln während der Compilierung. Tycho unterstützt alle Projekt Typen die auch von PDE unterstützt werden und wird, falls möglich, die PDE/JDT Projekt Metadaten verwenden. Ein wichtiger Anspruch von Tycho ist ja, Metadaten-Duplikate zwischen der pom.xml und den OSGi Metadaten zu vermeiden.

Hier mehr Informationen zum Tycho Build System: http://www.eclipse.org/tycho/

OSGi Rules werden also verwendet, um Projekt Dependencies aufzulösen.

Restfuse: Eine Open Source JUnit Erweiterung, um HTTP/REST APIs zu testen. Restfuse kann in einen Build genauso einfach integriert werden wie JUnit Tests.
Hier gibt es ein Beispiel Build-Projekt dazu:
https://github.com/eclipsesource/restfuse-tycho-integration

Neben der Möglichkeit, Applikationen aus Komponenten dynamisch zur Laufzeit zu kombinieren, unterstützt OSGi auch den gesamten Lebenszyklus der Komponenten, sowie eine Versionierung, was bedeutet, dass Komponenten zur Laufzeit installiert, gestartet, gestoppt und deinstalliert werden können. Somit ist es auch möglich, Komponenten zur Laufzeit durch eine andere Komponenten-Version auszutauschen. Das OSGi Framework stellt dabei sicher, dass die erforderlichen Abhängigkeiten beachtet werden.

Der Service und die Integrations-Tests

Es versteht sich von selbst, die Service Klassen Test-driven (TDD) zu entwickeln und bestimmte Grundfunktionalitäten mit JUnit Tests abzudecken, oder einen nicht-verfügbaren Service für die korrekte Ausführung des Tests durch Service Mock-Objekte zu ersetzen. Es ist aber ebenfalls wichtig, Integrations-Tests für den Service zu schreiben. Diese Tests ermöglichen die Überprüfung der aktuellen Verfügbarkeit und Funktionalität solcher Services auf der jeweiligen per DB-Connection oder mittels Portangabe konfigurierten Stage (Entwicklung, Test, Produktion). Um die Funktionalität des Services mittels Integrations-Test zu testen, wurde Restfuse verwendet. Restfuse ist, wie bereits erwähnt, eine JUnit Erweiterung für automatisierte HTTP/REST-Tests.

Bei dieser Gelegenheit darf auf das sehr gute Binaris Seminar zum Thema “TDD mit Java” von Binaris hier und hier hingewiesen werden, wo viele für die tägliche Programmierpraxis sehr gut zu übertragende Beispiele und nützliche Übungen durchlaufen und erklärt werden.

Ein REST-Service:

@Path( „/message“ )
public class ExampleService {
   @GET
   @Produces( MediaType.TEXT_PLAIN )
   public String getMessage() {
       return „DerService“;
   }
}

Ein simpler JUnit Test Case:

public class ExampleServiceTest {
   @Test
   public void testGetTheMessage() {
       ExampleService service = new ExampleService();
       String message = service.getMessage();    
       assertEquals(„DerService“, message);
   }
}

Die Service-Registrierung:

<?xml version=“1.0″ encoding=“UTF-8″?>
<scr:component xmlns:scr=“http://www.osgi.org/xmlns/scr/v1.1.0″
             name=“ExampleService“>
   <implementation class=“binaris.examples.ExampleService“/>
   <service>
       <provide interface=“binaris.examples.ExampleService“/>
   </service>
</scr:component>

Der Restfuse Integrations-Test:

@RunWith(HttpJUnitRunner.class)
public class ExampleServiceHttpTest {
 
   @Rule
   public Destination destination = new Destination( „http://localhost:9093“);
   @Context
   private Response response;
         
   @HttpTest(method = Method.GET, path = „/services/message“)
   public void checkTheMessage() {
      String body = response.getBody(String.class);
      assertOk(response);
      assertEquals(MediaType.TEXT_PLAIN, response.getType());
      assertEquals(„DerService“, body);
   }
}

Der laufende Service:

Es ist also erforderlich, damit der Integrations Test ausgeführt werden kann, zuvor den Server zu starten. Wenn dies vergessen wird, kann der Test logischerweise nur zum Service-Request passende (Connection) Timeouts als Antwort liefern. Um dies zu vermeiden, ohne den Service selbst vor einer Integrations-Testsequenz zu starten, kann die PDE JUnit Start Konfiguration verwendet werden, da eine solche Konfiguration entsprechend aufgesetzt werden kann, um den Server mit dem Service zu starten (falls dieser noch nicht gestartet ist), gegen den der Integrations-Test laufen soll.

Hierfür wird eine JUnit Plug-ins Test Suite entweder angelegt oder ausgewählt, die alle Integrations-Tests enthält, die ausgeführt werden sollen. Es können natürlich auch alle Tests eines ausgewählten Projekts, Packages oder Sourcecode-Verzeichnisses ausgeführt werden. In diesem Blog-Beispiel soll jedoch eine Test Suite verwendet, innerhalb der ein einzelner Test ausgeführt wird. Danach wechselt man in den „Main“ Tab Folder der JUnit Plug-ins Test Suite bzw. deren Junit Plugin Test Start-Konfiguration, wählt den Radio Button „Run an application“ und wählt als Application „[No application] – Headless Mode“. Als Letztes sind die dem Programm übergebenen Argumente zu konfiguren, die vom Server verwendet werden, in diesem Testfall also die Port Definition.

Die Bundle-Auswahl im Tab Folder „Plug-ins“ enthält dabei dieselben Bundles wie die einer OSGi Start-Konfiguration, die verwendet wird, um den Server mit dem REST-Service Standalone zu starten und zusätzlich die JUnit-, PDE JUnit-, Restfuse-Bundles und deren Dependencies.

Als Arguments also unter anderem:

-os ${target.os} –ws ${target.ws} –arch ${target.arch} –nl ${target.nl}

Und als VM arguments:

-Dorg.osgi.service.http.port=9093
-Dorg.eclipse.equinox.http.jetty.log.stderr.threshold=info

Die ausgewählte Test Suite würde dann so aussehen:

@RunWith(Suite.class)
@SuiteClasses({
ExampleServiceHttpTest.class
})
public class RestApiIntegrationTestSuite {
 
public static String BASE_URL = „http://localhost:“ +
   System.getProperty(„org.osgi.service.http.port“);
}
 

Das einzig ungewöhnliche hierbei ist die Definition der BASE_URL Konstante. Wie bereits erwähnt, wird der Server Port des ausgeführten Tests als Programm-Argument dem Test in der Start-Konfiguration übergeben. Aber Restfuse-Tests müssen den Port für die folgende „Destination Rule“- Definition ebenfalls kennen. Hiermit kann der Port in der Konfiguration geändert werden, ohne die Tests zu beeinflussen. Die Konstante wird dann einfach als Parameter in der folgenden Definition verwendet:

@Rule
public Destination destination = new Destination(BASE_URL);

In einem realen Anwendungsfall würde man eine separate Konstanten-Klasse verwenden, um die einzelnen Tests von der Test Suite mit ihrer jeweiligen importierten Konstanten-Klasse zu entkoppeln. Die statische BASE_URL Konstante trägt mittels static import im Restfuse Integrations-Test jedenfalls zur Verbesserung der Lesbarkeit des Sourcecodes bei.

Wenn man die Start-Konfiguration noch in einem separaten Projekt speichert, ermöglicht dies anderen Team-Mitgliedern, diese Start-Konfiguration ohne Duplizierung ebenfalls zu verwenden.

Diese Setup-Konfiguration der Integrations-Tests kann den eigenen Ablauf, wie Integrations-Tests lokal ausgeführt werden, also verbessern.

Restfuse OSGi Service-Test mit der Jetty HttpService-Implementierung

Hier ein voll funktionsfähiger HTTP Test:

@RunWith(HttpJUnitRunner.class)
public class RestfuseTest {
 
    @Rule
    public Destination destination = new Destination(„http://testdomain.de“);
    
    @Context
    private Response response; // wird nach jedem Request injectet

    @HttpTest(method = Method.GET, path = „/“)
    public void checkTestdomainRestfuseOnlineStatus() {
            assertOk(response);
    }
}

Hiermit wird zur Ausführungszeit ein Request an testdomain.degesendet, bevor die Test-Methode checkTestdomainRestfuseOnlineStatus() ausgeführt wird. Nach dem der Request ausgeführt wurde, wird dann die zurückgelieferte Response in das @Context annotierte response Attribut injectet, bevor die Test-Methode ausgeführt wird. Dieses Feld kann dann innerhalb der Test-Methode verwendet werden, um den Inhalt der Response zu überprüfen.

Die REST Services werden innerhalb einer OSGi Instanz veröffentlicht, wobei der OSGi-JAX-RS Connector verwendet wird, was jedoch optional ist, da die REST Services auch anders veröffentlicht werden können.

Wie bereits erwähnt, ist die prinzipielle Vorgehensweise, dass eine OSGi Instanz gestartet werden muss und die REST Services auch veröffentlicht werden müssen, damit die Tests gegen die Services ausgeführt werden können.

Bei der Verwendung von Tycho ist dies eine simple Aufgabe für ein Build-Skript, da Tycho den eclipse-test-plugin Packaging Type verwendet, um den Großteil dieser Aufgabe durchzuführen.

Im Beispiel gibt es ein Bundle de.binaris.restfuse.example.service. Dieses Bundle veröffentlicht einen REST Service mittels OSGI-JAX-RS Connector unter Registrierung der @Path annotierten Services. Die zugehörigen Restfuse Integrations Tests sind im de.binaris.restfuse.example.test Bundle, was in diesem Fall kein Fragment ist. Die Parent pom.xml des Tycho Builds befindet sich im Projekt de.binaris.restfuse.example.build. Die Parent pom.xml hat dabei innerhalb der repositories einen Abschnitt, wo das restfuse p2 repository hinzugefügt werden muss, um die Restfuse Tests zu starten.

<repositories>
  <repository>
    <id>juno</id>
    <layout>p2</layout>
    <url>http://download.eclipse.org/releases/juno</url>
  </repository>
  <repository>
    <id>osgi-jaxrs</id>
    <layout>p2</layout>
    <url>http://hstaudacher.github.com/osgi-jax-rs-connector</url>
  </repository>
  <repository>
    <id>restfuse</id>
    <layout>p2</layout>
    <url>http://download.eclipsesource.com/technology/restfuse/p2</url>
  </repository>
</repositories>

Der Build Abschnitt der Test-Plugin pom.xml ist dabei der interessante Teil. Hier muss die OSGi Umgebung eingetragen werden. Der eclipse-test-plugin Packaging Type kann Bundle Dependencies dann automatisch auflösen. Bei der Verwendung impliziter Abhängigkeiten, ist es erforderlich, noch mehr in den Build Abschnitt einzutragen. Im Beispiel gibt es drei implizite Abhängigkeiten, nämlich die folgenden Dependencies

– org.eclipse.equinox.ds
– com.eclipsesource.osgi.jaxrs.connector (JAX-RS REST-Connector)
– org.eclipse.equinox.http.jetty (die Jetty HttpService-Implementierung)

Eine weitere nicht ganz so offenschtliche implizite Abhängigkeit existiert im Bundle de.binaris.restfuse.example.test . Da hier Integrations-Tests erstellt werden, um das HTTP-Interface zu testen, gibt es erstmal keine direkte Abhängigkeit vom Sourcecode im Application Bundle de.binaris.restfuse.example.service . Auch diese Dependency wird zum Bundle hinzugefügt, damit Tycho das Bundle mit seinen Abhängigkeiten auflösen kann und die Tests ausführen kann.

Nach dem Eintragen der Dependencies, muss auch die OSGi Instance konfiguriert werden. Das bedeutet, dass die Bundles gestartet werden müssen und die Start Levels und Properties gesetzt werden müssen. Dabei müssen die Bundles mit den impliziten Dependencies zuerst gestartet werden, damit die davon abhängigen Bundles ebenfalls gestartet werden können. Als nächstes ist explizit ein viertes Bundle starten, nämlich das Application Bundle. Die zu setzende Property betrifft den Port für den HttpService org.osgi.service.http.port und wird, wie bereits erwähnt, ebenfalls verwendet um den Port für die @Rule annotierte Destination zu setzen.

<build>
  <plugins>
    <plugin>
      <groupId>${tycho-groupid}</groupId>
      <artifactId>tycho-surefire-plugin</artifactId>
      <version>${tycho-version}</version>
      <configuration>
        <argLine>-Dorg.osgi.service.http.port=12345</argLine>
        <showEclipseLog>true</showEclipseLog>
        <bundleStartLevel>
          <bundle>
            <id>org.eclipse.equinox.ds</id>
            <level>1</level>
            <autoStart>true</autoStart>
          </bundle>
          <bundle>
            <id>org.eclipse.equinox.http.jetty</id>
            <level>2</level>
            <autoStart>true</autoStart>
          </bundle>
          <bundle>
            <id>com.eclipsesource.jaxrs.connector</id>
            <level>3</level>
            <autoStart>true</autoStart>
          </bundle>
          <!– Start bundles required for the REST service –>
          <bundle>
            <id>de.binaris.restfuse.example.service</id>
            <level>4</level>
            <autoStart>true</autoStart>
          </bundle>
        </bundleStartLevel>
        <dependencies>
          <dependency>
            <type>p2-installable-unit</type>
            <artifactId>org.eclipse.equinox.ds</artifactId>
            <version>1.4.0</version>
          </dependency>
          <dependency>
            <type>eclipse-feature</type>
            <artifactId>org.eclipse.equinox.core.feature</artifactId>
            <version>1.1.0</version>
          </dependency>
          <dependency>
            <type>eclipse-feature</type>           
            <artifactId>org.eclipse.equinox.compendium.sdk</artifactId>
            <version>3.8.0</version>
          </dependency>
          <dependency>
            <type>eclipse-feature</type>
            <artifactId>org.eclipse.equinox.server.jetty</artifactId>
            <version>1.1.0</version>
          </dependency>
          <dependency>
            <type>p2-installable-unit</type>
            <artifactId>com.eclipsesource.jaxrs.connector</artifactId>
            <version>2.0.0</version>
          </dependency>
        </dependencies>
      </configuration>
    </plugin>
  </plugins>
</build>

Wie erkennbar ist, wurden ausser den impliziten Dependencies die folgenden weiteren Abhängigkeiten hinzugefügt:

– org.eclipse.equinox.core.feature
– org.eclipse.equinox.compendium.sdk
– org.eclipse.equinox.server.jetty (Jetty Server für den REST-Service)

um die erforderlichen Runtime Dependencies bereitzustellen.

Um den Tycho Build zu starten, kann Eclipse m2e tooling verwendet werden, unter Verwendung der Start-Konfiguration im de.binaris.restfuse.example.build Package oder per Maven mvn clean verify bzw. mvn clean build aus demselben Verzeichnis aufgerufen werden. Bei Verwendung des Surefire-Plugins befinden sich die Test Ergebnis-Reports nach Beendung des Tests im Unterverzeichnis de.binaris.restfuse.example.build/target/surefire-reports.

Unabhängig davon, welche asynchronen Mechanismen verwendet werden, gibt es nur zwei mögliche Optionen: entweder Polling oder Callback-Funktionen, die beide von RESTfuse unterstützt werden.

Polling

Wenn ein asynchroner Service mehr als einmal aufgerufen werden soll, kann die @Poll Annotation verwendet werden. Ein einfaches Beispiel kann z. B, folgendermaßen aussehen. Der Service in diesem Beispiel wird fünfmal so oft wie die Test-Methode aufgerufen werden. Die Response wird für jeden Request in das Test-Object injectet werden und kann dort per assertEquals(…) überprüft werden:

@RunWith( HttpJUnitRunner.class )
public class RestfusePollTest {

    @Rule
    public Destination destination = new Destination(„http://binaris.de“ );

    @Context
    private Response response;

    @Context
    private PollState pollState

    @HttpTest( method = Method.GET, path = „/asynchron“ )
    @Poll( times = 5, interval = 500 )
    public void testAsynchronousService() {
        Response currentResponse = pollState.getRespone(pollState.getTimes() );
        assertEquals( currentResponse, response );
    }
}

Callbacks

Um eine Callback-Funktion zu verwenden, um einen asynchronen Service zu testen, kann die @Callback Annotation für die Test-Methode verwendet werden. RESTFuse wird für den definierten Port einen Server starten und eine Resource registrieren. Der Test wird fehlschlagen, wenn die Resource nicht aufgerufen wird. Sonst kann innerhalb der Resource der eingehende Request überprüft werden. Hier ein entsprechender Test dazu:

@RunWith( HttpJUnitRunner.class )
public class RestfuseCallBack_Test {

    @Rule
    public Destination destination = new Destination( „http://binaris.de“ );

    @Context
    private Response response;

    private class TestCallbackResource extends DefaultCallbackResource {

        @Override
            public Response post( Request request ) {
            assertNotNull( request.getBody() );
            return super.post( request );
        }
    }

    @HttpTest( method = Method.GET, path = „/test“ )
    @Callback( port = 9090, path = „/asynchron“, resource =
                            TestCallbackResource.class, timeout = 10000 )
    public void testMethod() {
        assertAccepted( response );
    }
}

Hierzu auch die Empfehlung, das sehr gute   TDD mit Java   Seminar von Binaris

hier   und   hier   einmal zu buchen und zu besuchen.

Allen interessierten Leserinnen und Lesern noch einen schönen Halloween und Allerheiligen.

Kommentare

Einen Kommentar hinzufügen...

Sie müssen registriert und angemeldet sein um einen Kommentar zu schreiben.