Services – TDD mit Arquillian, REST, JSF2, CDI, JEE6

Die Motivation

Single Page Web-Applikationen (SPAs) haben nicht nur wegen Ihrer guten Laufzeit-Performance in JEE Web-Applikationen zunehmend an Bedeutung gewonnen. Diese können durchaus auch mit JSF 2 Implementierungen, wie dem RichFaces 4 umgesetzt werden und müssen nicht immer responsiv funktionieren. Primefaces Mobile Implementierungen können also verwendet werden, wenn eine mobile Version erforderlich ist, ansonsten ist eine RichFaces 4 Open Source Version mit der ihr eigenen Template-Technologie ebenfalls eine gute Lösung, die für Tablets und größere Displays auch gut funktioniert. Über Test-getriebene Entwicklung von SPAs mit responsivem Frontend (mittles Backbone, jQuery, QUnit, Selenium, etc.) gibt es in diesem Blog bereits Einträge und ich möchte bei dieser Gelegenheit auf das folgende sehr interessante, erfolgreiche und empfehlenswerte Seminar von Binaris Informatik hinweisen:

“TDD mit Java”

In diesem Blog-Eintrag zum Thema „Test-driven Development mit SPAs“ soll ausser den Beispielen zu Java/JEE-implementierten integrativen EJB Backend Service-Tests mittels dem Arquillan-Testframework auch eine simple Grusskarten Applikation implementiert werden, welche mittels JSF2 (RichFaces 4) funktioniert und eine Grusskarten-Mail mit dem Link auf ein Grusskartenbild versendet (Auswahl aus dem ‘Primefaces Gallerie‘  Komponenten-Beispiel). Zur Verbesserung der Usability kommen dabei HTML5 und CSS3 (Transitionen) zum Einsatz, ohne jedoch auf Animationen mit jQuery zurückzugreifen.

TDD der Beispiel Applikation

Wie zusätzliche Selenium-Tests mit dem Selenium FireFox-PlugIn durchgeführt und diese Tests gespeichert werden können, um mit solchen integrativen, automatisierten Tests ein paar Grusskarten zu versenden, ist bereits in diesem Blog-Eintrag hier beschrieben, um auch später jederzeit die korrekte Funktionsweise der Eingabe-Aktionen und Link-Aufrufe der SPA Applikation automatisiert über die erstellte Selenium Test-Suite verifizieren zu können.

Die Technologien:

Die Beispiel-Applikation ‘grusskarten‘ verwendet im Frontend JSF2 (RichFaces 4) und HTML5-/CSS3, sowie die .xhtml-Template Technologie und für das JAX-RS Service-Backend Java EE (Stateless EJB 3.1 und JEE 6 mittels CDI) auf dem ‘JBoss AS 7.4‘ mit der Produktbezeichnung ‘JBoss EAP 6.3‘ und auf dem WildFly 8.1.

Für die Weiterentwicklung, den Build und das Deployment des Projekts ist mindestens Java 6 und Maven 3 erforderlich. Als  Entwicklungsumgebung wurde Eclipse 4.3 (Kepler) in Form des “JBoss Developer Studios 7.1“ mit Java 7 verwendet und die FireFox WebDeveloper IDE/Tools.

Hier die verwendete pom.xml (mit der optionalen Primefaces Dependency):

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>de.binaris.grusskarten</groupId>
    <artifactId>grusskarten</artifactId>
    <version>1.0.0-SNAPSHOT</version>
    <packaging>war</packaging>
    <name>Binaris Greetingcards</name>
    <description>A starter Java EE 6 webapp project for use on JBoss AS 7 / EAP 6, generated from the jboss-javaee6-webapp archetype</description>

	<url>http://repository.jboss.org/nexus/content/groups/public</url>
    <properties>
        <!-- Explicit declaration of source encoding eliminates the following 
            message: -->
        <!-- [WARNING] Using platform encoding (UTF-8 actually) to copy filtered 
            resources, i.e. build is platform dependent! -->
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>

        <!-- JBoss dependency versions -->
        <version.org.jboss.as.plugins.maven.plugin>7.2.Final</version.org.jboss.as.plugins.maven.plugin>
        <!-- Define the version of the JBoss BOMs we want to import. The 
            JBoss BOMs specify tested stacks. -->
        <version.org.jboss.bom>1.0.0.Final</version.org.jboss.bom>
        <!-- Alternatively, comment out the above line, and un-comment the 
            line below to use version 1.0.0.Final-redhat-1 which is a release certified 
            to work with JBoss EAP 6. It requires you have access to the JBoss EAP 6 
            maven repository. -->
        <!-- <version.org.jboss.bom>1.0.0.Final-redhat-1</version.org.jboss.bom>> -->

        <!-- other plugin versions -->
        <version.compiler.plugin>2.3.1</version.compiler.plugin>
        <version.surefire.plugin>2.4.3</version.surefire.plugin>
        <version.war.plugin>2.1.1</version.war.plugin>

        <!-- maven-compiler-plugin -->
        <maven.compiler.target>1.6</maven.compiler.target>
        <maven.compiler.source>1.6</maven.compiler.source>
    </properties>

    <dependencyManagement>
        <dependencies>
            <!-- JBoss distributes a complete set of Java EE 6 APIs including 
                a Bill of Materials (BOM). A BOM specifies the versions of a "stack" (or 
                a collection) of artifacts. We use this here so that we always get the correct 
                versions of artifacts. Here we use the jboss-javaee-6.0-with-tools stack 
                (you can read this as the JBoss stack of the Java EE 6 APIs, with some extras 
                tools for your project, such as Arquillian for testing) and the jboss-javaee-6.0-with-hibernate 
                stack you can read this as the JBoss stack of the Java EE 6 APIs, with extras 
                from the Hibernate department of projects) -->
            <dependency>
                <groupId>org.jboss.bom</groupId>
                <artifactId>jboss-javaee-6.0-with-tools</artifactId>
                <version>${version.org.jboss.bom}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <dependency>
                <groupId>org.jboss.bom</groupId>
                <artifactId>jboss-javaee-6.0-with-hibernate</artifactId>
                <version>${version.org.jboss.bom}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>
    
    <dependencies>
        <!-- First declare the APIs we depend on and need for compilation. 
            All of them are provided by JBoss AS 7 -->
        <dependency>
            <groupId>javax.enterprise</groupId>
            <artifactId>cdi-api</artifactId>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.jboss.spec.javax.annotation</groupId>
            <artifactId>jboss-annotations-api_1.1_spec</artifactId>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.jboss.spec.javax.ws.rs</groupId>
            <artifactId>jboss-jaxrs-api_1.1_spec</artifactId>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.hibernate.javax.persistence</groupId>
            <artifactId>hibernate-jpa-2.0-api</artifactId>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.jboss.spec.javax.ejb</groupId>
            <artifactId>jboss-ejb-api_3.1_spec</artifactId>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-validator</artifactId>
            <scope>provided</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.slf4j</groupId>
                    <artifactId>slf4j-api</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <!-- Import the JSF API, we use provided scope as the API is included 
             in JBoss AS 7 -->
        <dependency>
            <groupId>org.jboss.spec.javax.faces</groupId>
            <artifactId>jboss-jsf-api_2.1_spec</artifactId>
            <scope>provided</scope>
        </dependency>
        <!-- Annotation processor to generate the JPA 2.0 metamodel classes 
             for typesafe criteria queries -->
        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-jpamodelgen</artifactId>
            <scope>provided</scope>
        </dependency>
        <!-- Annotation processor to raise compilation errors whenever 
             constraint annotations are incorrectly used. -->
        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-validator-annotation-processor</artifactId>
            <scope>provided</scope>
        </dependency>
        <!-- Needed for running tests (you may also use TestNG) -->
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <scope>test</scope>
        </dependency>
        <!-- Arquillian allows you to test enterprise code such as EJBs and 
            Transactional(JTA) JPA from JUnit/TestNG -->
        <dependency>
            <groupId>org.jboss.arquillian.junit</groupId>
            <artifactId>arquillian-junit-container</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.jboss.arquillian.protocol</groupId>
            <artifactId>arquillian-protocol-servlet</artifactId>
            <scope>test</scope>
        </dependency>
		<dependency>
			<groupId>javax.mail</groupId>
			<artifactId>mail</artifactId>
			<version>1.4.4</version>
		</dependency>
		<dependency>
			<groupId>javax.mail</groupId>
			<artifactId>javax.mail-api</artifactId>
			<version>1.5.2</version>
		</dependency>
		<dependency>
			<groupId>org.primefaces</groupId>
			<artifactId>primefaces</artifactId>
			<version>4.0</version>
		</dependency>
    </dependencies>

    <build>
        <!-- Maven will append the version to the finalName (which is the 
            name given to the generated war, and hence the context root) -->
        <finalName>${project.artifactId}</finalName>
        <plugins>
            <!-- Compiler plugin enforces Java 1.6 compatibility and activates 
                annotation processors -->
            <plugin>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>${version.compiler.plugin}</version>
                <configuration>
                    <source>${maven.compiler.source}</source>
                    <target>${maven.compiler.target}</target>
                </configuration>
            </plugin>
            <plugin>
                <artifactId>maven-war-plugin</artifactId>
                <version>${version.war.plugin}</version>
                <configuration>
                    <!-- Java EE 6 doesn't require web.xml, Maven needs to know -->
                    <failOnMissingWebXml>false</failOnMissingWebXml>
                </configuration>
            </plugin>
            <!-- To use, run: mvn package jboss-as:deploy -->
            <plugin>
                <groupId>org.jboss.as.plugins</groupId>
                <artifactId>jboss-as-maven-plugin</artifactId>
                <version>${version.org.jboss.as.plugins.maven.plugin}</version>
            </plugin>
        </plugins>
    </build>

    <profiles>
        <profile>
            <!-- The default profile skips all tests, though you can tune 
                 it to run just unit tests based on a custom pattern -->
            <id>default</id>
            <activation>
                <activeByDefault>true</activeByDefault>
            </activation>
            <build>
                <plugins>
                    <plugin>
                        <artifactId>maven-surefire-plugin</artifactId>
                        <version>${version.surefire.plugin}</version>
                        <configuration>
                            <skip>true</skip>
                        </configuration>
                    </plugin>
                </plugins>
            </build>
        </profile>
        <profile>
            <!-- An optional Arquillian testing profile that executes tests 
				the profile will start a new JBoss AS instance, execute 
                the test, shut it down when done -->
            <!-- Run with: mvn clean test -Parq-jbossas-managed -->
            <id>arq-jbossas-managed</id>
            <dependencies>
                <dependency>
                    <groupId>org.jboss.as</groupId>
                    <artifactId>jboss-as-arquillian-container-managed</artifactId>
                    <scope>test</scope>
                </dependency>
            </dependencies>
        </profile>
        <profile>
            <!-- An optional Arquillian testing profile that executes tests 
                in a remote JBoss AS instance -->
            <!-- Run with: mvn clean test -Parq-jbossas-remote -->
            <id>arq-jbossas-remote</id>
            <dependencies>
                <dependency>
                    <groupId>org.jboss.as</groupId>
                    <artifactId>jboss-as-arquillian-container-remote</artifactId>
                    <scope>test</scope>
                </dependency>
            </dependencies>
        </profile>
    </profiles>
    <repositories>
        <repository>
            <id>jboss</id>
            <name>JBoss Release Repository</name>
            <url>http://repository.jboss.org/maven2</url>
        </repository>
        <repository>
	        <id>jboss-snapshot</id>
	        <name>JBoss Maven Repo</name>
	        <url>http://snapshots.jboss.org/maven2</url>
	    </repository>
    </repositories>
</project>

Über CDI und JavaEE gibt es bereits Informationen in diesem Blog. Die Aktivierung von CDI erfolgt, wie beschrieben, mittels beans.xml im WEB-INF Verzeichnis.

Die Architektur:

a) Frontend:

Das JSF2 Servlet wird über die folgende faces-config.xml aktiviert:

<?xml version="1.0" encoding="UTF-8"?>
<!-- This file is optional for any extra configuration. -->
<faces-config version="2.0" xmlns="http://java.sun.com/xml/ns/javaee"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="
        http://java.sun.com/xml/ns/javaee
        http://java.sun.com/xml/ns/javaee/web-facesconfig_2_0.xsd">
    <!-- This descriptor activates the JSF 2.0 Servlet -->
    <!-- Navigation rules go here. Using CDI for creating @Named annotated managed beans. -->
    <!-- No vavigation rules required for a SPA with only one site -->
</faces-config>

Über CDI und JavaEE gibt es bereits Informationen in diesem Blog. Die Aktivierung von CDI erfolgt, wie beschrieben, mittels beans.xml im WEB-INF Verzeichnis.

Der Aufruf erfolgt über einen Redirect in der index.html:

<!-- Plain HTML page redirecting to index.jsf -->
<html>
<head>
        <meta http-equiv="Refresh" content="0; URL=index.jsf">
</head>
</html>

Das Template für die einzige SPA-Seite befindet sich unter /WEB-INF/templates/template.xhtml:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
    xmlns:h="http://java.sun.com/jsf/html"
    xmlns:ui="http://java.sun.com/jsf/facelets">
<h:head>
    <title>Greetingcards</title>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
    <h:outputStylesheet name="css/screen.css" />
</h:head>
<h:body>
    <div id="container">
        <div id="content">
            <ui:insert name="content">
            </ui:insert>
        </div>
        <div id="aside">
            <p>Learn more about JBoss Enterprise Application Platform 6.</p>
            <ul>
                <li><a href="https://access.redhat.com/knowledge/docs/JBoss_Enterprise_Application_Platform/">Documentation</a></li>
                <li><a href="http://red.ht/jbeap-6">Product Information</a></li>
            </ul>
            <p>Learn more about JBoss AS 7.</p>
            <ul>
                <li><a
                    href="http://www.jboss.org/jdf/quickstarts/jboss-as-quickstart/guide/Introduction">Getting
                        Started Developing Applications Guide</a></li>
                <li><a href="http://jboss.org/jbossas">Community
                        Project Information</a></li>
            </ul>
            <p>Learn more about TDD with Java <a href="http://binaris-informatik.de/?p=2275">here</a> and <a href="https://www.testdrivendevelopment.de/">here</a></p>
        </div>
        <div id="footer">
            <p>
                This project was generated from a Maven archetype from JBoss.<br />
            </p>
        </div>
    </div>
</h:body>
</html>

Die einzige Seite index.xhtml der SPA wird im div der id “content“ per <ui:insert>integriert:

<?xml version="1.0" encoding="UTF-8"?>
<ui:composition xmlns="http://www.w3.org/1999/xhtml"
    xmlns:ui="http://java.sun.com/jsf/facelets"
    xmlns:f="http://java.sun.com/jsf/core"
    xmlns:h="http://java.sun.com/jsf/html"
    template="/WEB-INF/templates/template.xhtml">
    <ui:define name="content">
        <h1>Welcome to Greetingcards!</h1>

        <div>
            <p>Greetingcards - a Java EE 6 Enterprise Application.</p>
            <h3>Application runs on:</h3>
            <img src="resources/gfx/dualbrand_as7eap.png" />
        </div>

        <h:form id="reg">
            <h2>Recipient and Greetingcard Registration</h2><br />
            <h:panelGrid columns="7" columnClasses="titleCell">
                <h:outputLabel for="name" value="Name:" />
                <h:inputText id="name" value="#{newRecipient.name}" />
                <h:message for="name" errorClass="invalid" />
                <h:message for="pathToCard" errorClass="invalid" />
                <h:message for="pathToCard" errorClass="invalid" />
                <h:message for="pathToCard" errorClass="invalid" />
                <h:message for="pathToCard" errorClass="invalid" />

                <h:outputLabel for="email" value="Email:" />
                <h:inputText id="email" value="#{newRecipient.email}" />
                <h:message for="email" errorClass="invalid" />
                <h:message for="pathToCard" errorClass="invalid" />
                <h:message for="pathToCard" errorClass="invalid" />
                <h:message for="pathToCard" errorClass="invalid" />
                <h:message for="pathToCard" errorClass="invalid" />

                <h:outputLabel for="message" value="Message:" />
                <h:inputText id="message"
                    value="#{newRecipient.message}" />
                <h:message for="message" errorClass="invalid" />
                <h:message for="pathToCard" errorClass="invalid" />
                <h:message for="pathToCard" errorClass="invalid" />
                <h:message for="pathToCard" errorClass="invalid" />
                <h:message for="pathToCard" errorClass="invalid" />
            </h:panelGrid>
            <h2>Choose a picture card to send it:</h2>
            <h:panelGrid columns="7">
            	<h:commandButton id="register1" action="#{recipientController.register('resources/gfx/galleria1.jpg')}" value="Register" styleClass="card" image="resources/gfx/galleria1.jpg" />
            	<h:commandButton id="register2" action="#{recipientController.register('resources/gfx/galleria2.jpg')}" value="Register" styleClass="card" image="resources/gfx/galleria2.jpg" />
            	<h:commandButton id="register3" action="#{recipientController.register('resources/gfx/galleria3.jpg')}" value="Register" styleClass="card" image="resources/gfx/galleria3.jpg" />
            	<h:commandButton id="register4" action="#{recipientController.register('resources/gfx/galleria4.jpg')}" value="Register" styleClass="card" image="resources/gfx/galleria4.jpg" />
            	<h:commandButton id="register5" action="#{recipientController.register('resources/gfx/galleria5.jpg')}" value="Register" styleClass="card" image="resources/gfx/galleria5.jpg" />
            	<h:commandButton id="register6" action="#{recipientController.register('resources/gfx/galleria6.jpg')}" value="Register" styleClass="card" image="resources/gfx/galleria6.jpg" />
            	<h:commandButton id="register7" action="#{recipientController.register('resources/gfx/galleria7.jpg')}" value="Register" styleClass="card" image="resources/gfx/galleria7.jpg" />
            </h:panelGrid>
            <h:panelGrid columns="1">
                <h:messages styleClass="messages"
                    errorClass="invalid" infoClass="valid"
                    warnClass="warning" globalOnly="true" />
            </h:panelGrid>
        </h:form>
        <h2>Recipients and Greetingcards sent:</h2>
        <h:panelGroup rendered="#{empty recipients}">
            <em>No registered recipients.</em>
        </h:panelGroup>
        <h:dataTable var="recipient" value="#{recipients}"
            rendered="#{not empty recipients}"
            styleClass="simpletablestyle">
            <h:column>
                <f:facet name="header">Id</f:facet>
                #{recipient.id}
            </h:column>
            <h:column>
                <f:facet name="header">Name</f:facet>
                #{recipient.name}
            </h:column>
            <h:column>
                <f:facet name="header">Email</f:facet>
                #{recipient.email}
            </h:column>
            <h:column>
                <f:facet name="header">Message</f:facet>
                #{recipient.message}
            </h:column>
            <h:column>
                <f:facet name="header">Card</f:facet>
                <h:button image="#{recipient.pathToCard}" styleClass="card" />
            </h:column>
            <h:column>
                <f:facet name="header">REST URL</f:facet>
                <a href="#{request.contextPath}/rest/recipients/#{recipient.id}">/rest/recipients/#{recipient.id}</a>
            </h:column>
            <f:facet name="footer">
            REST URL for all recipients: <a
                    href="#{request.contextPath}/rest/recipients">/rest/recipients</a>
            </f:facet>
        </h:dataTable>
    </ui:define>
</ui:composition>

Die register(…) Action des RecipientController wird durch einen Click auf ein Grusskarten Bild in der Gallerie aufgerufen, z. B.:  action=“#{recipientController.register(‚resources/gfx/galleria7.jpg‘)}“

Hier der RecipientController mit der auszuführenden Action:

package de.binaris.grusskarten.controller;

import java.util.HashMap;
import java.util.logging.Logger;

import javax.annotation.PostConstruct;
import javax.enterprise.inject.Model;
import javax.enterprise.inject.Produces;
import javax.faces.application.FacesMessage;
import javax.faces.context.FacesContext;
import javax.inject.Inject;
import javax.inject.Named;

import de.binaris.grusskarten.model.Recipient;
import de.binaris.grusskarten.service.RecipientRegistration;

@Model
public class RecipientController {

    @Inject
    private FacesContext facesContext;

    @Inject
    private RecipientRegistration recipientRegistration;

    @Produces
    @Named
    private Recipient newRecipient;

    @PostConstruct
    public void initNewRecipient() {
    	newRecipient = new Recipient();
    }
    
    public void register(String pathToCard) throws Exception {
        try {
            Logger log = Logger.getLogger("RecipientController");
            HashMap<String, String> cardsRecipient = new HashMap<String, String>();
            newRecipient.setPathToCard(pathToCard);
            cardsRecipient.put("mailRecipientName", newRecipient.getName());
            cardsRecipient.put("mailRecipientEmail", newRecipient.getEmail());
            cardsRecipient.put("message", newRecipient.getMessage());
            cardsRecipient.put("pathToCard", newRecipient.getPathToCard());
            recipientRegistration.setCardsRecipient(cardsRecipient);

            recipientRegistration.register(newRecipient);
        	
            log.info("register: cardsRecipient.get('"+"mailRecipientName"+"'): "+cardsRecipient.get("mailRecipientName"));
            log.info("register: cardsRecipient.get('"+"mailRecipientEmail"+"'): "+cardsRecipient.get("mailRecipientEmail"));
            log.info("register: cardsRecipient.get('"+"message"+"'): "+cardsRecipient.get("message"));
            log.info("register: cardsRecipient.get('"+"pathToCard"+"'): "+cardsRecipient.get("pathToCard"));
        	
            initNewRecipient();
        	
            FacesMessage m = new FacesMessage(FacesMessage.SEVERITY_INFO, "Recipient registered, card sent!", "Registration successful");
            facesContext.addMessage(null, m);
        } catch (Exception e) {
            String errorMessage = getRootErrorMessage(e);
            FacesMessage m = new FacesMessage(FacesMessage.SEVERITY_ERROR, errorMessage, "Registration unsuccessful");
            facesContext.addMessage(null, m);
        }
    }

    private String getRootErrorMessage(Exception e) {
        // error message when registration fails.
        String errorMessage = "Registration failed. See server log for more information";
        if (e == null) {
            // default error messages
            return errorMessage;
        }
        // find the root cause
        Throwable t = e;
        while (t != null) {
            // Get the message from the Throwable class instance
            errorMessage = t.getLocalizedMessage();
            t = t.getCause();
        }
        // root cause message
        return errorMessage;
    }
}

b) Service Backend:

Dabei entspricht die im Service-Backend verwendeten Stateless Session Beans (EJB 3.1) vom Design Pattern her der Facade (SLSB – Stateless Session Bean) für ein einheitliches Interface. Die SLSBs wird hier ohne deklarativ annotierte @Transaction oder zusätzliche User Transaction verwendet. Hier das RecipientRepository der für die Grusskarten verwendeten Empfänger unter Verwendung der JPA Recipient Entity und der benötigten Queries:

package de.binaris.grusskarten.data;

import javax.enterprise.context.ApplicationScoped;
import javax.inject.Inject;
import javax.persistence.EntityManager;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Root;

import java.util.List;

import de.binaris.grusskarten.model.Recipient;

@ApplicationScoped
public class RecipientRepository {

    @Inject
    private EntityManager em;

    public Recipient findById(Long id) {
        return em.find(Recipient.class, id);
    }

    public Recipient findByEmail(String email) {
        CriteriaBuilder cb = em.getCriteriaBuilder();
        CriteriaQuery<Recipient> criteria = cb.createQuery(Recipient.class);
        Root<Recipient> recipient = criteria.from(Recipient.class);
        // Swap criteria statements to try out type-safe criteria queries, a new
        // feature in JPA 2.0
        // criteria.select(member).where(cb.equal(member.get(Recipient_.name), email));
        criteria.select(recipient).where(cb.equal(recipient.get("email"), email));
        return em.createQuery(criteria).getSingleResult();
    }

    public List<Recipient> findAllOrderedByName() {
        CriteriaBuilder cb = em.getCriteriaBuilder();
        CriteriaQuery<Recipient> criteria = cb.createQuery(Recipient.class);
        Root<Recipient> recipient = criteria.from(Recipient.class);
        // Swap criteria statements to try out type-safe criteria queries, a new
        // feature in JPA 2.0
        // criteria.select(recipient).orderBy(cb.asc(member.get(Recipient_.name)));
        criteria.select(recipient).orderBy(cb.asc(recipient.get("name")));
        return em.createQuery(criteria).getResultList();
    }
}

Für die Verwendung von JPA 2.0 im obigen RecipientRepository generiert Maven mitels pom.xml im Verzeichnis target/generated-sources/de/binaris/grusskarten/model die abstrakte Klasse Recipient_java, die bei Bedarf dem Build Path hinzuzufügen ist, mit den folgenden Metadaten:

package de.binaris.grusskarten.model;

import javax.annotation.Generated;
import javax.persistence.metamodel.SingularAttribute;
import javax.persistence.metamodel.StaticMetamodel;

@Generated(value = "org.hibernate.jpamodelgen.JPAMetaModelEntityProcessor")
@StaticMetamodel(Recipient.class)
public abstract class Recipient_ {

	public static volatile SingularAttribute<Recipient, String> message;
	public static volatile SingularAttribute<Recipient, Long> id;
	public static volatile SingularAttribute<Recipient, String> email;
	public static volatile SingularAttribute<Recipient, String> name;
	public static volatile SingularAttribute<Recipient, String> pathToCard;

}

Hier nun der RecipientRegistration-Service (als SLSB):

package de.binaris.grusskarten.service;

import java.util.HashMap;
import java.util.logging.Logger;

import javax.ejb.Stateless;
import javax.enterprise.event.Event;
import javax.inject.Inject;
import javax.mail.MessagingException;
import javax.mail.internet.AddressException;
import javax.persistence.EntityManager;

import de.binaris.grusskarten.model.Recipient;
import de.binaris.grusskarten.util.MailSender;

// @Stateless annotation without explicit manual transaction demarcation
@Stateless
public class RecipientRegistration {

    @Inject
    private Logger recipientLogger;

    @Inject
    private EntityManager em;

    @Inject
    private Event<Recipient> recipientEventSrc;

    private HashMap<String, String> cardsRecipient;
    
    public void register(Recipient recipient) throws Exception {
    	recipientLogger.info("Registering " + recipient.getName());
        em.persist(recipient);
        recipientEventSrc.fire(recipient);
        
        recipientLogger.info("Registered. ");
        try {
	MailSender.sendMessage("mycards@yahoo.com", 
	"password", 
	cardsRecipient.get("mailRecipientName"),
	cardsRecipient.get("mailRecipientEmail"), 
	cardsRecipient.get("message"), 
	cardsRecipient.get("pathToCard"));
        } catch (AddressException e) {
	e.printStackTrace();
        } catch (MessagingException e) {
	e.printStackTrace();
        }
    }
    
    public void setCardsRecipient(HashMap<String, String> cardsRecipient) {
    	this.cardsRecipient = cardsRecipient;
    }
    
    public HashMap<String, String> getCardsRecipient() {
    	return cardsRecipient;
    }
}

c) Der REST Webservice:

Über den Einsatz von RESTful Webservices für SPAs unter Verwendung von JBoss RESTEasy gibt es bereits Blog-Einträge in diesem Blog. Die Aktivierung des RESTEasy-Webservices geschieht dort (Servlet 2.3, 2,4 oder 2.5) über die web.xml:

<?xml version="1.0"?>
<!DOCTYPE web-app PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
        "http://java.sun.com/dtd/web-app_2_3.dtd">

<web-app>
   <display-name>Archetype Created Web Application</display-name>
   <context-param>
      <param-name>javax.ws.rs.core.Application</param-name>
      <param-value>de.binaris.rest.samples.service.SumOfMultiplesApplication</param-value>
   </context-param>

   <context-param>
      <param-name>resteasy.servlet.mapping.prefix</param-name>
      <param-value>/resteasy</param-value>
   </context-param>

   <listener>
      <listener-class>
         org.jboss.resteasy.plugins.server.servlet.ResteasyBootstrap
      </listener-class>
   </listener>

   <servlet>
      <servlet-name>Resteasy</servlet-name>
      <servlet-class>
         org.jboss.resteasy.plugins.server.servlet.HttpServletDispatcher
      </servlet-class>
   </servlet>

   <servlet-mapping>
      <servlet-name>Resteasy</servlet-name>
      <url-pattern>/resteasy/*</url-pattern>
   </servlet-mapping>
</web-app>

Stattdessen wird nun die folgende Klasse JaxRsActivator verwendet, die von Application abgeleitet ist und mit der @ApplicationPath Annotation annotiert ist, da seit servlet-3.0 keine web.xml mehr benötigt wird, sondern optional alles über Annotationen deklarierbar ist (Hinweis: wenn beides verwendet wird .xml-Deklaration und Annotation, überschreibt die .xml-Deklaration die Funktionalität der Annotation):

package de.binaris.grusskarten.rest;

import javax.ws.rs.ApplicationPath;
import javax.ws.rs.core.Application;

@ApplicationPath("/rest")
public class JaxRsActivator extends Application {
    /* neither extended class attributes nor methods required */
}

Hier der Beispiel REST-Webservice zum Eintragen des Recipients und dem Versand der Card:

package de.binaris.grusskarten.rest;

import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.Logger;

import javax.enterprise.context.RequestScoped;
import javax.inject.Inject;
import javax.persistence.NoResultException;
import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;
import javax.validation.ValidationException;
import javax.validation.Validator;
import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;

import de.binaris.grusskarten.data.RecipientRepository;
import de.binaris.grusskarten.model.Recipient;
import de.binaris.grusskarten.service.RecipientRegistration;

/**
 * JAX-RS Example
 * <p/>
 * This class produces a RESTful service to read/write the contents of the recipients table.
 */
@Path("/recipients")
@RequestScoped
public class RecipientResourceRESTService {
    
    @Inject
    private Logger recipientLogger;

    @Inject
    private Validator validator;

    @Inject
    private RecipientRepository repository;

    @Inject
    RecipientRegistration registration;

    @GET
    @Produces(MediaType.APPLICATION_JSON)
    public List<Recipient> listAllRecipients() {
        return repository.findAllOrderedByName();
    }

    @GET
    @Path("/{id:[0-9][0-9]*}")
    @Produces(MediaType.APPLICATION_JSON)
    public Recipient lookupRecipientById(@PathParam("id") long id) {
        Recipient recipient = repository.findById(id);
        if (recipient == null) {
            throw new WebApplicationException(Response.Status.NOT_FOUND);
        }
        return recipient;
    }

    /**
     * Creates a new member from the values provided. Performs validation, and will return a JAX-RS response with either 200 ok,
     * or with a map of fields, and related errors.
     */
    @POST
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_JSON)
    public Response createRecipient(Recipient recipient) {

        Response.ResponseBuilder builder = null;
        try {
            // Validates member using bean validation
            validateRecipient(recipient);
            registration.register(recipient);

            // Create an "ok" response
            builder = Response.ok();
        } catch (ConstraintViolationException ce) {
            // Handle bean validation issues
            builder = createViolationResponse(ce.getConstraintViolations());
        } catch (ValidationException e) {
            // Handle the unique constrain violation
            Map<String, String> responseObj = new HashMap<String, String>();
            responseObj.put("email", "Email taken");
            builder = Response.status(Response.Status.CONFLICT).entity(responseObj);
        } catch (Exception e) {
            // Handle generic exceptions
            Map<String, String> responseObj = new HashMap<String, String>();
            responseObj.put("error", e.getMessage());
            builder = Response.status(Response.Status.BAD_REQUEST).entity(responseObj);
        }
        return builder.build();
    }

    /**
     * <p>
     * Validates the given Recipient variable and throws validation exceptions based on the type of error. If the error is standard
     * bean validation errors then it will throw a ConstraintValidationException with the set of the constraints violated.
     * </p>
     * <p>
     * If the error is caused because an existing recipient with the same email is registered it throws a regular validation
     * exception so that it can be interpreted separately.
     * </p>
     * 
     * @param recipient Recipient to be validated
     * @throws ConstraintViolationException If Bean Validation errors exist
     * @throws ValidationException If member with the same email already exists
     */
    private void validateRecipient(Recipient recipient) throws ConstraintViolationException, ValidationException {
        // Create a bean validator and check for issues.
        Set<ConstraintViolation<Recipient>> violations = validator.validate(recipient);

        if (!violations.isEmpty()) {
            throw new ConstraintViolationException(new HashSet<ConstraintViolation<?>>(violations));
        }
        // Check the uniqueness of the email address
        if (emailAlreadyExists(recipient.getEmail())) {
//            throw new ValidationException("Unique Email Violation");
        }
    }

    /**
     * Creates a JAX-RS "Bad Request" response including a map of all violation fields, and their message. This can then be used
     * by clients to show violations.
     * 
     * @param violations A set of violations that needs to be reported
     * @return JAX-RS response containing all violations
     */
    private Response.ResponseBuilder createViolationResponse(Set<ConstraintViolation<?>> violations) {
    	recipientLogger.fine("Validation completed. violations found: " + violations.size());
        Map<String, String> responseObj = new HashMap<String, String>();

        for (ConstraintViolation<?> violation : violations) {
            responseObj.put(violation.getPropertyPath().toString(), violation.getMessage());
        }
        return Response.status(Response.Status.BAD_REQUEST).entity(responseObj);
    }

    /**
     * Checks if a recipient with the same email address is already registered. 
     * This is the only way to easily capture the
     * "@UniqueConstraint(columnNames = "email")" constraint from the Recipient class.
     * 
     * @param email The email to check
     * @return True if the email already exists, and false otherwise
     */
    public boolean emailAlreadyExists(String email) {
        Recipient recipient = null;
        try {
        	recipient = repository.findByEmail(email);
        } catch (NoResultException e) {
            // ignore
        }
        return recipient != null;
    }
}

Die Yahoo Mail Account-Session, um die Grusskarten Mail über den Account userMail zu versenden, erhält man folgendermaßen:

/**
 * get a Yahoo mail account session to send the email via account userMail
 * 
 * @param userMail
 * @return Session
 */
public static Session getMailSenderSessionYahoo(String userMail) {
	
    final Properties props = System.getProperties();
    props.setProperty( "mail.smtp.from", userMail);
    props.setProperty( "mail.smtp.host", "smtp.mail.yahoo.com" );
    props.setProperty( "mail.smtp.auth", "true" );
    props.setProperty( "mail.smtp.port", "465" );
    props.setProperty( "mail.smtp.starttls.enable", "true");
    props.setProperty( "mail.smtp.quitwait", "false");
    props.setProperty( "mail.smtp.ssl.enabled", "true");
    props.setProperty( "mail.smtp.socketFactory.port", "465" );
    props.setProperty( "mail.smtp.socketFactory.class","javax.net.ssl.SSLSocketFactory" );
    return Session.getInstance( props, null);
}

d) Die Datenbank:

Die Testdaten aus der import.sql:

insert into Recipient (id, name, email, message, pathToCard) values (0, 'Banana Joe', 'banana.joe@bananajoe.com', 'Hey Joe, wazzuuup... call me.', 'resources/gfx/galleria1.jpg')

Die persistence.xml aus dem /META-INF Unterverzeichnis zur Aktivierung von JPA 2 sieht für die Verwendung der HSQL Beispiel-Datenbank folgendermaßen aus:

<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.0"
   xmlns="http://java.sun.com/xml/ns/persistence" 
   xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
   xsi:schemaLocation="http://java.sun.com/xml/ns/persistence
        http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd">
   <persistence-unit name="grusskarten">
      <jta-data-source>java:jboss/datasources/GrusskartenDS</jta-data-source>
      <properties>
         <property name="hibernate.hbm2ddl.auto" value="create-drop" />
         <property name="hibernate.show_sql" value="false" />
      </properties>
   </persistence-unit>
</persistence>

Die Verwendung von JPA 2 ist am folgenden <persistence …>-Element sehr gut zu erkennen. Denn ohne dass hier die version=“2.0“ eingetragen ist, fährt man logischerweise stets nur persistence 1.0, d.h. z.B. JPA 1.

<persistence version="2.0"
   xmlns="http://java.sun.com/xml/ns/persistence" 
   xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"   xsi:schemaLocation="http://java.sun.com/xml/ns/persistence    http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd"> 

Für eine produktive Applikation und Datenbank sollte die Datasource in der standalone.xml entsprechend konfiguriert werden.

Für den Blog-Eintrag der Beispiel-Applikation soll die Beschreibung der Verwendung der HSQL “in memory“-Datenbank genügen. Auf dem Testserver ist dann eine MySQL Server Datenbank installiert und die entsprechende Datasource konfiguriert. Hier der Link zur Beispiel-Applikation:

http://educationorganizer.de:8485/grusskarten/index.html

Hierfür wird im /WEB-INF-Verzeichnis neben der beans.xml (CDI-Aktivierung) auch die folgende grusskarten-ds.xml hinterlegt:

<?xml version="1.0" encoding="UTF-8"?>
<!-- unmanaged datasource for testing purposes only uses H2, 
        an in memory database that ships with JBoss AS. -->
<datasources xmlns="http://www.jboss.org/ironjacamar/schema"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.jboss.org/ironjacamar/schema   
    http://docs.jboss.org/ironjacamar/schema/datasources_1_0.xsd">

    <!-- The datasource is bound into JNDI at this location. We reference 
        this in META-INF/persistence.xml -->
    <datasource jndi-name="java:jboss/datasources/GrusskartenDS"
        pool-name="grusskarten" enabled="true"
        use-java-context="true">
        <connection-url>
jdbc:h2:mem:grusskarten;DB_CLOSE_ON_EXIT=FALSE;DB_CLOSE_DELAY=-1
        </connection-url>
        <driver>h2</driver>
        <security>
            <user-name>admin</user-name>
            <password>banana_joe</password>
        </security>
    </datasource>
</datasources>

Abschließend noch die Die CSS3-Styles in der screen.css mit den Transitionen in der Klasse .card und dem Style für das .hover-Event über einer Grusskarte .card:hover

screen.css:

/* Core styles for the page */
body {
  margin: 0;
  padding: 0;
  background-color: #4a5d75;
  font-family: "Lucida Sans Unicode", "Lucida Grande", sans-serif;
  font-size: 0.8em;
  color:#363636;
}

#container {
  margin: 0 auto;
  padding: 0 20px 10px 20px;
  border-top: 5px solid #000000;
  border-left: 5px solid #8c8f91;
  border-right: 5px solid #8c8f91;
  border-bottom: 25px solid #4a5d75;
  width: 865px; /* subtract 40px from banner width for padding */
  background: #d5d5d5;
  background-repeat: repeat-x;
  padding-top: 30px;
  box-shadow: 3px 3px 15px #d5d5d5;
}
#content {
  float: left;
  width:500px;
  margin: 20px;
}
#aside {
  font-size: 0.9em;
  width: 275px;
  float: left;
  margin: 20px 0px;
  border: 1px solid #D5D5D5;
  background: #F1F1F1;
  background-image: url(#{request.contextPath}/resources/gfx/asidebkg.png);
  background-repeat: repeat-x;
  padding: 20px;
}
.card {
  font-size: 0.9em;
  transition: all 0.3s ease-in-out 0.5s;
  width: 25px;
  height: 20px;
  float: left;
  margin: 0px 0px;
  border: 1px solid #D5D5D5;
  background: #F1F1F1;
  background-repeat: repeat-x;
  padding: 2px;
}

#aside ul {
  padding-left: 30px;
}
.dualbrand {
  float: right;
  padding-right: 10px;
}
#footer {
  clear: both;
  text-align: center;
  color: #666666;
  font-size: 0.85em;
}
code {
  font-size: 1.1em;
}
a {
  color: #4a5d75;
  text-decoration: none;
}
a:hover {
  color: #369;
  text-decoration: underline;
}
.card:hover {
  transition: all 0.5s ease-in-out;
  width: 130px;
  height: 100px;
}
h1 {
  color:#243446;
  font-size: 2.25em;
}
h2 {
  font-size: 1em;
}
h3 {
  color:#243446;
}
h4 {
}
h5 {
}
h6 {
}
/* Recipient registration styles */
span.invalid {
  padding-left: 3px;
  color: red;
}
form {
  padding: 1em;
  font: 80%/1 sans-serif;
  width: 375px;
  border: 1px solid #D5D5D5;
  background: #F1F1F1;
}
label {
  float: left;
  width: 15%;
  margin-left: 20px;
  margin-right: 0.5em;
  padding-top: 0.2em;
  text-align: right;
  font-weight: bold;
  color:#363636;
}
input {
  margin-bottom: 8px;
}
.register {
  float: left;
  margin-left: 85px;
}

.simpletablestyle {
  background-color:#E6E7E8;
  clear:both;
  width: 550px;
}

.simpletablestyle img {
  border:0px;
}

.simpletablestyle td {
  height:2em;
  padding-left: 6px;
  font-size:11px;
  padding:5px 5px;
}

.simpletablestyle th {
  background: url(#{request.contextPath}/resources/gfx/bkg-blkheader.png) black repeat-x top left;
  font-size:12px;
  font-weight:normal;
  padding:0 10px 0 5px;
  border-bottom:#999999 dotted 1px;
}

.simpletablestyle thead {
  background: url(#{request.contextPath}/resources/gfx/bkg-blkheader.png) black repeat-x top left;
  height:31px;
  font-size:10px;
  font-weight:bold;
  color:#FFFFFF;
  text-align:left;
}

.simpletablestyle .header a {
  color:#94aebd;
}

.simpletablestype tfoot {
  height: 20px;
  font-size: 10px;
  font-weight: bold;
  background-color: #EAECEE;
  text-align: center;
}

.simpletablestyle tr.header td {
  padding: 0px 10px 0px 5px;
}

.simpletablestyle .subheader {
  background-color: #e6e7e8;
  font-size:10px;
  font-weight:bold;
  color:#000000;
  text-align:left;
}

/* Using new CSS3 selectors for styling*/
.simpletablestyle tr:nth-child(odd) {
  background: #f4f3f3;
}
.simpletablestyle tr:nth-child(even) {
  background: #ffffff;
}

.simpletablestyle td a:hover {
  color:#3883ce;
  text-decoration:none; 
}

Die Test-Frameworks und die Tests

a) Die Arquillian Tests für das Service-Backend werden folgendermaßen aktiviert:

Das /test-Verzeichnis neben dem /WEB-INF-Verzeichnis des grusskarten.war hat hierfür folgende Struktur:

- src/test/
    - resources
        - META-INF   
            test-persistence.xml
        - arquillian.xml
        - grusskarten-test-ds.xml
    - java
        - de.binaris.grusskarten.test
            RecipientRegistrationTest.java

Mit der folgenden test-persistence.xml:

<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.0" 
   xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd">
   <persistence-unit name="grusskarten">
      <!-- We use a different datasource for tests, so as to not overwrite 
         production data. This is an unmanaged data source, backed by H2, an in memory 
         database. Production applications should use a managed datasource. -->
      <!-- The datasource is deployed as WEB-INF/test-ds.xml, 
         you can find it in the source at src/test/resources/grusskarten-test-ds.xml -->
      <jta-data-source>java:jboss/datasources/GrusskartenTestDS</jta-data-source>
      <properties>
         <!-- Properties for Hibernate -->
         <property name="hibernate.hbm2ddl.auto" value="create-drop" />
         <property name="hibernate.show_sql" value="false" />
      </properties>
   </persistence-unit>
</persistence>

Und der folgenden arquillian.xml:

<?xml version="1.0" encoding="UTF-8"?>
<arquillian xmlns="http://jboss.org/schema/arquillian" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://jboss.org/schema/arquillian http://jboss.org/schema/arquillian/arquillian_1_0.xsd">
   <!-- Force the use of the Servlet 3.0 protocol with all containers, as it is the most mature -->
   <defaultProtocol type="Servlet 3.0" />
   <!-- Example configuration for a remote JBoss AS 7 instance -->
   <container qualifier="jboss" default="true">
      <!-- If you want to use the JBOSS_HOME environment variable, just delete the jbossHome property -->
      <configuration>
         <property name="jbossHome">/path/to/jboss/as</property>
      </configuration>
   </container>
</arquillian>

Hier die grusskarten-test-ds.xml:

<?xml version="1.0" encoding="UTF-8"?>
<!-- This is an unmanaged datasource. It should be used for proofs of concept 
   or testing only. It uses H2, an in memory database that ships with JBoss 
   AS. -->
<datasources xmlns="http://www.jboss.org/ironjacamar/schema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
   xsi:schemaLocation="http://www.jboss.org/ironjacamar/schema http://docs.jboss.org/ironjacamar/schema/datasources_1_0.xsd">
   <!-- The datasource is bound into JNDI at this location. We reference 
      this in META-INF/test-persistence.xml -->
   <datasource jndi-name="java:jboss/datasources/GrusskartenTestDS"
      pool-name="grusskarten-test" enabled="true"
      use-java-context="true">
      <connection-url>jdbc:h2:mem:grusskarten-test;DB_CLOSE_DELAY=-1</connection-url>
      <driver>h2</driver>
      <security>
         <user-name>admin</user-name>
         <password>banana_joe</password>
      </security>
   </datasource>
</datasources>

Und hier die Test-Klasse RecipientRegistrationTest für den Backend Service-Test:

package de.binaris.grusskarten.test;

import static org.junit.Assert.assertNotNull;
import java.util.logging.Logger;
import javax.inject.Inject;

import org.jboss.arquillian.container.test.api.Deployment;
import org.jboss.arquillian.junit.Arquillian;
import org.jboss.shrinkwrap.api.Archive;
import org.jboss.shrinkwrap.api.ShrinkWrap;
import org.jboss.shrinkwrap.api.asset.EmptyAsset;
import org.jboss.shrinkwrap.api.spec.WebArchive;
import org.junit.Test;
import org.junit.runner.RunWith;

import de.binaris.grusskarten.model.Recipient;
import de.binaris.grusskarten.service.RecipientRegistration;
import de.binaris.grusskarten.util.Resources;

@RunWith(Arquillian.class)
public class RecipientRegistrationTest {
    @Deployment
    public static Archive<?> createTestArchive() {
        return ShrinkWrap.create(WebArchive.class, "test.war")
                .addClasses(Recipient.class, RecipientRegistration.class, Resources.class)
                .addAsResource("META-INF/test-persistence.xml", "META-INF/persistence.xml")
                .addAsWebInfResource(EmptyAsset.INSTANCE, "beans.xml")
                // deploy the test datasource
                .addAsWebInfResource("grusskarten-test-ds.xml");
    }

    @Inject
    RecipientRegistration recipientRegistration;

    @Inject
    Logger recipientLogger;

    @Test
    public void testRegister() throws Exception {
        Recipient newRecipient = new Recipient();
        newRecipient.setName("Django Doe");
        newRecipient.setEmail("django@mailinator.com");
        newRecipient.setMessage("Hey Django, please call me.");
        newRecipient.setPathToCard("resources/gfx/galleria1.jpg");
        recipientRegistration.register(newRecipient);
        assertNotNull(newRecipient.getId());
        recipientLogger.info(newRecipient.getName() + " was persisted with id " + newRecipient.getId());
    }
}

In der pom.xml können unter den Profilen im Default Profil für den skip-Test Wert ‘false‘ eingetragen werden und z. B. der Maven Task  mvn clean test -Parq -jbossas-managed  aufgerufenwerden, damit die Tests ausgeführt werden. Hier noch mal der relevante Abschnitt in der pom.xml:

   <profiles>
        <profile>
            <!-- The default profile skips all tests, though you can tune 
                 it to run just unit tests based on a custom pattern -->
            <id>default</id>
            <activation>
                <activeByDefault>false</activeByDefault>
            </activation>
            <build>
                <plugins>
                    <plugin>
                        <artifactId>maven-surefire-plugin</artifactId>
                        <version>${version.surefire.plugin}</version>
                        <configuration>
                            <skip>true</skip>
                        </configuration>
                    </plugin>
                </plugins>
            </build>
        </profile>
        <profile>
            <!-- An optional Arquillian testing profile that executes tests 
				the profile will start a new JBoss AS instance, execute 
                the test, shut it down when done -->
            <!-- Run with: mvn clean test -Parq-jbossas-managed -->
            <id>arq-jbossas-managed</id>
            <dependencies>
                <dependency>
                    <groupId>org.jboss.as</groupId>
                    <artifactId>jboss-as-arquillian-container-managed</artifactId>
                    <scope>test</scope>
                </dependency>
            </dependencies>
        </profile>
        <profile>
            <!-- An optional Arquillian testing profile that executes tests 
                in a remote JBoss AS instance -->
            <!-- Run with: mvn clean test -Parq-jbossas-remote -->
            <id>arq-jbossas-remote</id>
            <dependencies>
                <dependency>
                    <groupId>org.jboss.as</groupId>
                    <artifactId>jboss-as-arquillian-container-remote</artifactId>
                    <scope>test</scope>
                </dependency>
            </dependencies>
        </profile>
    </profiles>

b) Die Selenium Tests:

Installation: In der Test-Browser Instanz die folgende Installations-Url aufrufen und der Installation zustimmen:

http://release.seleniumhq.org/selenium-ide/2.9.0/selenium-ide-2.9.0.xpi

Selenium-Blog:

http://blog.reallysimplethoughts.com/2015/03/09/selenium-ide-scheduler-has-arrived-part-1/

Selenium Download:

http://docs.seleniumhq.org/download/

Selenium Documentation:

http://docs.seleniumhq.org/docs/
https://seleniumhq.github.io/docs/
http://seleniumhq.github.io/selenium/docs/api/java/index.html

Die Tests werden folgendermaßen angelegt, nachdem das Selenium IDE PlugIn installiert wurde. Dabei sollten folgende Grundregeln beachtet werden:

– Tests sollten immer eine definierte Start-Url haben, z.B. hier: http://localhost:8080/grusskarten/

– Tests sollten nicht von anderen Tests abhängig sein.

– Tests sollten immer nur eine Sache/einen kleinen Workflow je Test Case testen,

z.B. der Versand einer Card.

– Test Cases können in einer Test Suite zusammengefasst werden, um den Automatisierungsgrad zu erhöhen.

Bei dieser Gelegenheit am Ende des Blog-Eintrags darf auch erneut auf das sehr effektive, interessante und erfolgreiche Seminar

“TDD mit Java” von Binaris Informatik

hingewiesen werden.

Wie Test-Suites, die mit dem Selenium IDE PlugIn durchgeführt wurden und automatisiert ausgeführt werden können, erstellt werden, wurde bereits in einem Blog Eintrag diese Blogs beschrieben.

Hier nun auch das komplette Web-Archiv grusskarten.war zum Download: http://www.4shared.com/file/YxqB4JjRba/grusskarten.html

Hier der JBoss EAP 6.3 bzw. WildFly 8.1 (JBoss AS 7.4) als Jar-Installer zum Download.

http://www.jboss.org/download-manager/file/jboss-eap-6.3.0.GA-installer.jar

Hier noch eine weitere SPA (Single Page Appliction) Beispiel-Applikation, bei der das Testen des Backends mittels Arquillian und das integrative Testen des Frontends mittels Selenium (Workflow und UX Elemente) und QUnit  (Javascript-Framework zum Test-Aufruf der REST-Endpoints aus einer Browser-View) ebenfalls ausführlich beschrieben ist.

Installation/Start der Beispiel-Applikation:

– Das JBoss Zip-Archiv entpacken

– JAVA_HOME, JBOSS_HOME entsprechend setzen und

in der standalone.bat oder standalone.sh verwenden.

– grusskarten.war ins Server-Unterverzeichnis /standalone/deployments speichern

– Server starten per standalone.bat oder standalone.sh, http://localhost:8080/grusskarten/ aufrufen

– Die Selenium Tests erstellen per Selenium IDE PlugIn/Recorder erstellen und ausführen.

Allen interessierten Leserinnen und Lesern weiterhin viel Freude bei der agilen Softwareentwicklung mittels Scrum und dem Test Driven Development mit Java, sowie ein angenehmes und fröhliches “Super Bowl“ Wochenende und eine verantwortungsvolle Karnevals-Session.