Donnerstag, 16. September 2010

Three Steps to Test Driven Enteprise JavaBeans

Enterprise JavaBeans (EJBs) sind Komponenten, die in einem Applikationsserver laufen. EJBs werden deshalb als "managed" bezeichnet. Der Applikationsserver stellt den EJBs Laufzeitdienste für die Transaktionssteuerung, Synchronisation, Pooling und rollenbasierte Sicherheit zur Verfügung.

EJBs sind Geschäftskomponenten, die keine Benutzeroberfläche haben.  EJBs implementieren konkrete Geschäftsfunktionalitäten und sind im Backend über lokale Schnittstellen miteinander vernetzt. EJB 3 vereinfacht mit Hilfe von Dependency Injection (DI) den Zusammenbau des Komponentennetzwerkes. Im Komponentennetzwerk sind die EJBs auf konkrete "Use Cases" eines Geschäftsmodells zugeschnitten. Das Komponentennetzwerk mündet häufig in einer oder mehreren Facaden, welche die Schnittstellen zur Außenwelt repräsentieren. Facaden sind grobkörnig ("Coarse Grained") und mit einer Schnittstelle für den entfernten Zugriff ausgestattet. 

In der Regel wird pro EJB eine Testsuite erstellt. Unit-Tests auszuführen ist kein Problem, weil in der Entwicklungsumgebung der Applikationsserver lokal mitläuft. Der Applikationsserver hält dabei  Verbindungen zu Datenbanken und anderen externen Systemen als Ressourcen vor. Nur in Ausnahmefällen sind Mock-Objekte für externe Schnittstellen zu programmieren, die nicht über den in der Entwicklungsumgebung lokal installierten Applikationsserver zu erreichen sind.

Im optimalen Fall sind alle externen Ressourcen verfügbar und eine lokale bzw. entfernte Datenbankverbindung konfiguriert. Grundsätzlich ist es empfehlenswert, die Unit-Tests gegen die Facade einer Applikation laufen zu lassen. Vorausgesetzt, dass die EJBs, die in der Facade münden, ebenfalls zuvor mit Unit-Tests abgesichert worden sind. Testläufe gegen die Facade sind wesentlich, weil später in Produktion alle Clients über die Facade das Komponentennetzwerk erreichen wollen.

Bei diesen Bedingungen stellt sich die berechtigte Frage, warum EJBs bei der Entwicklung gemockt werden sollen? Im Fall der EJB-Schnittstellen sind Mock-Objekte interessant, weil vorab ohne den Applikationsserver zu berücksichtigen Schnittstellen designed, getestet und abgestimmt werden können. Für die in einem EJB implementierten Algorithmen gilt das gleiche wie, bei herkömmlichen Java Applikationen, sodass Mock-Objekte für die Programmierung konkreter Algorithmen im Inneren eines EJBs einzusetzen sind.

Für die Gegner von Mocks, die aber Unit-Tests mögen (solche Kandidaten gibt es), bietet sich die Methode der "Setter Injection" an. "EJB Setter" in Unit-Tests anzuwenden ist seit Java EE 5 möglich. Man bedenke, EJBs sind ab Version 3 leichtgewichtige Komponenten (POJOs).

Beispiel - Setter Injection

@Stateless
public class AccountBean implements Account {

    private User user;

    @EJB
    public void setUser(User user) {

         this.user = user;
    }
}

Die Methode "setUser(User user)" kann in einem Unit-Tests verwendet werden, um das User-Bean zu initialisieren. Im Applikationsserver hingegen wird das User-Bean automatisch per Dependency Injection vom EJB-Container initialisiert. Setter Injection löst das Problem der Testbarkeit von EJBs, die zum Zeitpunkt des Testlaufes nicht im EJB-Container laufen sollen. Unabhängig von dieser Testform wird strengstens empfohlen die EJBs auch in der "managed" Umgebung des Applikationsservers zu testen.

Zurück zu den Mock-Objekten (Fakes). Der Mock-Framework Mockito eignet sich für die Erzeugung von Fakes. Bei der Entwicklung des Szenarios für den Blogbeitrag wurde Mockito verwendet, um die Schnittstelle für einen Account Service zu implementieren.

Die Implementierung des Account Service beinhaltet drei Schritte
  1. Fake
    Design und Implementierung der Account Service Schnittstelle mit Mockito

  2. Programmierung
    Umsetzen der Account Service Schnittstelle mit EJB 3

  3. Unit-Test
    Test der deployten Account Service Schnittstelle mit einem Unit-Test
  4.  
     Schritt 1 - Fake

    package ccd.jee.session.beans.account;

    import static org.junit.Assert.assertFalse;
    import static org.junit.Assert.assertTrue;
    import static org.mockito.Mockito.mock;
    import static org.mockito.Mockito.when;
    import org.junit.Before;
    import org.junit.Test;
    import ccd.jee.session.beans.account.domain.AccountUser;

    public class AccountServiceDesign {

        private AccountUser user;
        private Account account;
      
        @Before
        public void setUp() {
           
            setUpUser();      
            setUpAccount();
        }
      
        @Test
        public void testCreateUser() {

            final boolean result = account.createUser(user);
            assertTrue(result);
        }
         
        @Test
        public void testLogin() {

            final boolean result = account.login(user);
            assertTrue(result);
        }
      
        @Test
        public void testLoginFailed() {

            final AccountUser loginUser = mock(AccountUser.class);
            when(account.login(loginUser)).thenReturn(false);
            final boolean result = account.login(loginUser);      
            assertFalse(result);
        }
      
        @Test
        public void testDeleteUser() {

            final boolean result = account.deleteUser(user);
            assertTrue(result);
        }

        private void setUpUser() {
           
            user = mock(AccountUser.class);
            when(user.getAlias()).thenReturn("alias");
            when(user.getPassword()).thenReturn("password");
        }

        private void setUpAccount() {
           
            account = mock(Account.class);
            when(account.login(user)).thenReturn(true);
            when(account.createUser(user)).thenReturn(true);
            when(account.deleteUser(user)).thenReturn(true);
        }
    }

    Schritt 2 - Programmierung

    Schnittstelle des Account Service:

    package ccd.jee.session.beans.account;

    import javax.ejb.Remote;
    import ccd.jee.session.beans.account.domain.AccountUser;

    @Remote
    public interface Account {

        public boolean login(AccountUser user);
        public boolean createUser(AccountUser user);
        public boolean deleteUser(AccountUser user);
    }

    Entität eines Benutzers:

    package ccd.jee.session.beans.account.domain;

    import java.io.Serializable;
    import javax.persistence.Entity;
    import javax.persistence.GeneratedValue;
    import javax.persistence.Id;
    import javax.persistence.NamedQuery;

    @Entity
    @NamedQuery(name = "findUser",
                query="SELECT o FROM AccountUser o WHERE o.alias = :alias AND o.password = :password")
    public class AccountUser implements Serializable {

        @Id
        @GeneratedValue  
        private int id;
      
        private String alias;
        private String password;
      
        public String getAlias() {
      
            return alias;
        }
      
        public String getPassword() {

            return password;
        }
      
        public void setAlias(String alias) {
            this.alias = alias;
        }

        public void setPassword(String password) {
            this.password = password;
        }

        public int getId() {
            return id;
        }
    }

    Implementierung des Account Service:

    package ccd.jee.session.beans.account;

    import javax.annotation.PostConstruct;
    import javax.ejb.Stateless;
    import javax.persistence.EntityManager;
    import javax.persistence.NoResultException;
    import javax.persistence.PersistenceContext;
    import javax.persistence.Query;
    import org.apache.log4j.Logger;
    import ccd.jee.session.beans.account.domain.AccountUser;

    @Stateless(mappedName="ejb/AccountBean")
    public class AccountBean implements Account {

        @PersistenceContext
        private EntityManager em;

        private Logger logger;
      
        @PostConstruct
        public void setUp() {
          
            logger = Logger.getRootLogger();
        }
      
        @Override
        public boolean createUser(AccountUser user) {
          
            if(userNotExists(user)) {
              
                em.persist(user);                  
                return true;
            }
                      
            return false;
        }
      
        @Override
        public boolean deleteUser(AccountUser user) {
                      
            if(userExists(user)) {
                  
                em.remove(user);              
                return true;
            }
          
            return false;
        }

        @Override
        public boolean login(AccountUser user) {
                  
            return userExists(user);
        }

        private boolean userExists(AccountUser user) {
          
            return !userNotExists(user);
        }
      
        private boolean userNotExists(AccountUser user) {
          
            try {
              
                findUser(user);          
            }  
            catch(NoResultException ex) {
                  
                logger.debug("User not exists: " + user.getAlias());
                return true;
            }
          
            logger.debug("User exists: " + user.getAlias());
            return false;
        }
      
        private AccountUser findUser(AccountUser user) {
          
            final Query query = em.createNamedQuery("findUser");
            query.setParameter("alias", user.getAlias());
            query.setParameter("password", user.getPassword());
          
            return (AccountUser) query.getSingleResult();
        }
    }

     Schritt 3 - Unit-Test

    package ccd.jee.session.beans.account;

    import static org.junit.Assert.assertFalse;
    import static org.junit.Assert.assertTrue;
    import javax.naming.Context;
    import javax.naming.InitialContext;
    import javax.naming.NamingException;
    import org.junit.BeforeClass;
    import org.junit.Test;
    import ccd.jee.session.beans.account.domain.AccountUser;

    public class AcountServiceTest {

        private static final String JNDI_NAME = "ejb/AccountBean";
        private static Context context;
        private static Account account;

        @BeforeClass
        public static void setUp() throws NamingException {
          
            context = new InitialContext();      
            account = (Account) context.lookup(JNDI_NAME);  
        }
      
        @Test
        public void testCreateUser() {
          
            final AccountUser user = getAccountUser();
          
            final boolean result = account.createUser(user);      
            assertTrue(result);
        }

      
        @Test
        public void testLogin() {
              
            final AccountUser loginUser = getAccountUser();
          
            final boolean result = account.login(loginUser);
            assertTrue(result);
        }
      
        @Test
        public void testLoginFailed() {
              
            final AccountUser loginUser = getAccountUser("wrong-alias","wrong-password");
          
            final boolean result = account.login(loginUser);
            assertFalse(result);
        }
      
        @Test
        public void testDeleteUser() {
          
            final AccountUser user = getAccountUser();
          
            final boolean result = account.deleteUser(user);
            assertTrue(result);
        }
      
        private AccountUser getAccountUser(String alias, String password) {
          
            final AccountUser user = new AccountUser();
            user.setAlias(alias);
            user.setPassword(password);
          
            return user;
        }

        private AccountUser getAccountUser() {
          
            return getAccountUser("alias", "password");
        }
    }

    Die Vorgehensweise in drei Schritten hat sich bewährt. Im ersten Schritt konnte mit dem Fake die Schnittstelle des Account Service und die Entität des Benutzers angelegt und mit Mockito getestet werden. Auf Basis der erzeugten Artifakte wurde im zweiten Schritt die EJB-Programmierung vorgenommen. In der Folge ist im dritten Schritt das Deployment auf dem Glassfish v3 für Java EE 6 erfolgt und ein entsprechender Unit-Test gegen die entfernte Schnittstelle des Account Service geschrieben worden.

    TDD konform wurde zunächst die Schnittstelle des Account Service entworfen, abgestimmt und getestet. Im Account Service erkennt man eine Symmetrie zwischen dem Anlegen und dem Löschen eines Benutzers. Beim Löschen  wird geprüft, ob der Benutzer vorhanden ist. Beim Anlegen hingegen, ob noch kein Benutzer mit dem gleichen Aliasnamen und Passwort existiert.

    Das umgesetzte Szenario des Account Service ist sicherlich nicht komplex. Das Prinzip der drei Schritte ist allerdings auch in komplexeren Szenarien anwendbar. Vorteil dabei ist, dass die Schnittstellen von EJBs früh getestet und abgestimmt werden können, ohne die eigentliche Programmierung der EJBs begonnen zu haben.


    Der Rechtshinweis des Java Blog für Clean Code Developer ist bei der Verwendung und Weiterentwicklung des Quellcodes des Blogeintrages zu beachten.