Ejb3unit

Egyre inkább fenntartásokkal kezelem azokat a rendszereket, ahol az alkalmazás és az operációs rendszer között több köztes réteg (middleware) is elhelyezkedik.

Már egyszerű Java programok írásánál is előjön a JVM, amivel könnyen meg lehet barátkozni, de azért az első időkben nem indult be olyan könnyedén, az appletek elterjedését nagyban gátolta a mérete, telepítésével kapcsolatos nehézségek és inkompatibilitási problémák. De a mai gépeken, valamint a JVM fejlesztésével már elfogadható sebességgel fut, de azért emlékezzünk az időkre, mikor egy több (akár száz) ablakból indítható Delphi vagy Visual Basic alkalmazásunk milyen gyorsan elindult. De az ember megbarátkozik vele az általa nyújtott előnyök miatt, mint a platformfüggetlenség és magas biztonság.

Ha webes alkalmazásokat kell fejlesztenünk, akkor már web konténert kell alkalmaznunk, amikkel szintén viszonylag kevés probléma van, leszámítva a az alkalmazások többszöri újratelepítésekor előforduló java.lang.OutOfMemoryError: PermGen space hibákat, mely azt jelzi, hogy a régi web alkalmazást valami ok miatt nem tudta kitakarítani a memóriából. Igaz ugyan, hogy ez az alkalmazás miatt van, de klasszikus programozási mintákat követve is találkozzunk vele. Érdemes ekkor valamilyen profiler eszközt elővenni.

A következő szint, mikor már a alkalmazásszervert vetünk be, hogy nagyvállalati igényeinket kielégítsük, vagy ágyúval lőjünk verébre. Na ez az a szint, aminek bevezetésén már alaposan el kell gondolkozni, mert nem feltétlenül használjuk ki az alkalmazásszerver előnyeit, de rengeteg hibába ütközhetünk, lásd egy korábbi posztomban. Sajnos az alkalmazásszerver gyártók, ahelyett, hogy kijavítanák a hibákat, és egy stabil verzió irányába mennének, vagy nagyon lassan moccannak, vagy annyira az új funkciókra koncentrálnak a nagy versenyben, hogy nincs egy stabil verzió.

A következő szint az integrációs (EAI, SOA) szint, mely újabb szoftver rétegeket vezet be, mint az ESB, service registry, workflow motor az orchestration megvalósítására, valamint a rules engine-ek. Ezt is lehetne jól csinálni, de sajnos mivel ekkora infrastruktúra bevezetésére csak nagy cégek, telekom szolgáltatók, bankok, stb. képesek, az egész a szoftver licensz eladás lobbiba torkollik, és általában nem egy pilot projektre támaszkodva, a megfelelőség és tudás alapján döntenek. Másik probléma vele, hogy mind a grafikusan megrajzolható üzleti folyamatok, mind a szabály motorok bevezetése egy (nem mindig) leplezett célt szolgál, tudjuk az üzleti folyamatainkat fejlesztői tudás nélkül is módosítani, továbbfejleszteni. Ez egy olyan infrastruktúrával jár, mely fenntartása, üzemeltetése sokkal költségesebb, és a fejlesztésnél még magasabb szintű technológiai tudás kell, ami ritkább és drágább. Azaz költség megtakarításból költség növelés lesz.

És ezt még bonyolítja az alkalmazáson belüli rétegek és keretrendszerek (nem egy esetben felesleges) használata.

Tapasztalva ezen magas szintű eszközök által biztosított fejlesztési folyamat (compile, package, deploy) lassúságát fordult a figyelmem a konténeren kívüli tesztelés felé, hiszen így a fejlesztett funkciók gyorsabban futtathatóak és tesztelhetőek. Alapvetően nem hiszek a Test Driven Development (TDD) fejlesztésben egyszerű, pl. CRUD alkalmazások fejlesztésekor, viszonylag egyszerű webes rendszereknél. Nagyobb létjogosultsága lehet pl. bonyolult üzleti folyamatok, valamint szélesebb körben terjesztett 3rd party eszközök fejlesztésekor. Nem tartom szükségesnek a 100%-os lefedettségi teszt elérését sem, az accessor metódusok tesztelését, stb.

Viszont a különböző funkciók konténer nélküli indítására, tesztelésére, amire a JUnit-os idők előtt esetleg gyorsan összetákoltam egy main függvényt, arra most javasolt egy újrafelhasználható JUnit teszt esetet írni.

Az EJB3 komponensek tesztelésére különböző eszközök állnak a rendelkezésünkre. Egyik lehetőség natív JUnit kóddal, esetleg Mock objektumokkal való tesztelés, amikor a dependency injection-t mi oldjuk fel. De használhatunk valamilyen segítséget is erre, ilyen pl. a külön is, alkalmazásszerveren kívül futtatható, beágyazható EJB konténer, mint az OpenEJB, vagy a JBoss Embeddable EJB 3.0. Másik megoldás a cikkem főszereplője, az Ejb3unit, mely egy JUnit-ra épülő EJB-k és entitások (entity bean-eknek nevezi, szerintem tévesen) tesztelését támogató keretrendszer.

Nézzünk is egy példát erre. Képzeljük el, hogy van egy banki alkalmazásunk két session bean-nel. Az egyikkel (BankEJB) lehet számlát ügyfelet felvenni, számlát nyitni, számlát lekérdezni, stb., a másik session bean (TransferEJB) végzi az átutalásokat az előbbi bean meghívásával. Az alkalmazásban szerepel két entitás is 1:n kapcsolatban (az egyszerűség kedvéért), az ügyfél (Client) és a számla (Account). A példa bemutatja a két távoli üzleti interfészt, de a bean osztályok (TransferEJBBean és ebből a @EJB annotációval hivatkozott BankEJBBean) és a JPA entitások megvalósítását az olvasó képzeletére bízom. A BankEJB távoli üzleti interfész:
@Remote
public interface BankEJB {

 public Client createClient(String name);
  
 public Account openAccount(Client client, long balance);

 public void credit(long accountId, long balance);

 public Account findAccountById(long accountId);

  ...
}
A TransferEJB távoli üzleti interfész:
@Remote
public interface TransferEJB {

 public void doTransfer(long srcAccountId, long destAccountId, long amount);
  
 ...
}
A bean-ek teszteléséhez a következő teszt esetet készíthetjük el:
public class TransferEJBBeanTest extends BaseSessionBeanFixture {

   private static final Class[] usedBeans = {Account.class, Client.class};

   public TransferEJBBeanTest() {
       super(TransferEJBBean.class, usedBeans);
   }

   public void testTransfer() throws NamingException {
       TransferEJB transferEJB = (TransferEJB) getBeanToTest();

       Context c = new InitialContext();
       BankEJB bankEJB = (BankEJB) c.lookup("ejb/BankEJBBean");

       getEntityManager().getTransaction().begin();
       Client client = bankEJB.createClient("Client");

       Account srcAccount = bankEJB.openAccount(client.getId(), 1000);
       Account destAccount = bankEJB.openAccount(client.getId(), 2000);

       transferEJB.doTransfer(src.getId(), dst.getId(), 500);

       srcAccount = bankEJB.findAccountById(src.getId());
       destAccount = bankEJB.findAccountById(dst.getId());

       assertEquals(srcAccount.getBalance(), 500);
       assertEquals(destAccount.getBalance(), 2500);

       getEntityManager().getTransaction().rollback();
   }
}
A példában látható, hogy a BaseSessionBeanFixture osztályt kell kiterjeszteni, és az abban definiált konstruktort kell meghívni a tesztelendő bean osztályával és a tesztelendő entitásokkal. Nagyon fontos, hogy a többi bean-t JNDI-ből lehet lekérni. Fontos, hogy a tranzakciót manuálisan kell indítani. A rollback a végén azért szerepel, mert így a teszt esetnek semmi mellékhatása nem lesz, így nem marad semmi az adatbázisban. Az Ejb3unit automatikusan elvégzi a bean-ek példányosítását, a dependency injection-t és a perzisztenciát. Ezt konfigurálni a classpath-ban lévő ejb3unit.properties állománnyal lehet, melynek tartalma:
ejb3unit.inMemoryTest=true
ejb3unit.show_sql=true

ejb3unit_jndi.1.isSessionBean=true
ejb3unit_jndi.1.jndiName=ejb/BankEJBBean
ejb3unit_jndi.1.className=bankejb.beans.BankEJBBean

ejb3unit_jndi.2.isSessionBean=true
ejb3unit_jndi.2.jndiName=ejb/TransferEJBBean
ejb3unit_jndi.2.className=bankejb.beans.TransferEJBBean

ejb3unit.loadPersistenceXML=true
ejb3unit.persistenceUnit.name=BankEJB

Ennek pontosságára figyeljünk, mert a hibaüzenet sajnos nem mindig nyomravezető. Ebben az esetben egy memóriában futó relációs adatbázis (Derby) indul be, és az SQL parancsokat is képes naplózni. Utána a session bean-ek konfigurációja történik (osztály neve, session bean-e, JNDI neve), majd meg lehet adni, hogy töltse be a persistence.xml állományt is, megadva az abban definiált persistence unit nevét.

A jndi a classpath-ban lévő jndi.properties állománnyal konfigurálható:

java.naming.factory.initial=com.bm.jndi.MemoryContextFactory

Ekkor a JNDI fa a memóriában fog elhelyezkedni.

A példát ha NetBeans-ben akarjuk kipróbálni, akkor figyelni kell arra, hogy a test környvtárban elhelyezett két (ejb3unit.properties, jndi.properties) állományt egyszerű futtatás esetén nem másolja a build/test/classes könyvtárba, így nem fogja azokat megtalálni. Érdekes, hogy a nbproject/build-impl.xml fájlban a -do-compile-test target-ben benne van, de a -do-compile-test-single target-ben viszont nincs, ezt pótolva futtatható a teszt esetünk.

Ennél az Ejb3unit többet is tud, pl. entitások automatikus példányosítását, véletlen adatokkal való feltöltését, perzisztálását és visszaolvasását, vagy az adatbázis feltöltését CSV fájl alapján az entitásokon keresztül.

Az Ejb3unit egyik általam felfedezett hiányossága pl., hogy a teszt eseteinkben nem használhatunk DI annotációkat. Erre a JTechnics blogban láttam példát, de sajnos csak a Unitils keretrendszerhez.