JUnit testen van EJB3/JPA code met een in memory database
Junit testen van EJB code is altijd al lastig geweest. Het probleem is dat je een container nodig hebt om je testen goed te kunnen draaien. Er zijn wel in container testing frameworks, maar die zijn vrij zwaar en ondersteunen niet echt het idee van een Junit test: een stuk code dat je geïsoleerd en snel kunt testen. In dit artikel wordt een opzet beschreven voor het Junit testen van EJB3/JPA code op basis van een in memory database.
JPA en HSQLDB als in memory database
Met EJB3 is het gelukkig allemaal wat eenvoudiger geworden. Met de introductie van de entity manager is het ineens mogelijk geworden deze te gebruiken om Junit testen te schrijven. Maar om te kunnen JUnit testen, moet je niet afhankelijk zijn van een database die beschikbaar moet zijn en gevuld met correcte testdata. Als testcode afhankelijk is van zo’n database, is er sprake van een integratie test.
Het idee is dan ook om een in memory database te gebruiken die wordt ondersteund door JPA en deze zodanig te configureren dat de database on the fly wordt gecreëerd bij het starten van de Junit test. Door een in memory database te gebruiken, kan dit erg snel. In dit voorbeeld maken we gebruik van de HSQLDB Database. In de setUp van de Junit test wordt de database opgestart en in de tearDown weer gesloten.
JPA Testcase
Het opzetten van zo’n test case kan bijvoorbeld als volgt. Schrijf een abstract base class die extend van TestCase. Deze basis class voor de testen kan de setUp en tearDown logica bevatten voor de JPA entityManager. Hieronder wordt een voorbeeld gegeven van een applicatie die 2 services heeft. Een EmployeeService die afhankelijk is van JPA en een Mail service waarvoor een Mock implementatie wordt geinstantieerd. Deze mock implementatie kan uiteraard ook met een framework als EasyMock worden opgezet.
Bijvoorbeeld:
public abstract class JpaBaseTest extends TestCase // Declareer als protected zodat de subclasses van de Junit testen gebruik kunnen maken van deze service. protected EmployeeService; public JpaBaseTest(String testName) { super(testName); // Instantieer de te testen services en injecteer ze. employeeService = new EmployeeServiceImpl(); mailService = new MailMock(); }
In de setup injecteren we de entity manager en configureren we alle gebruikte services. Hier worden dus in feite de taken van de EJB Container overgenomen.
@Override protected void setUp() throws Exception { super.setUp(); try { log.info("Start de in-memory HSQL database voor de unit tests"); Class.forName("org.hsqldb.jdbcDriver"); connection = DriverManager.getConnection("jdbc:hsqldb:mem:unit-testing-jpa", "sa", ""); } catch (Exception ex) { ex.printStackTrace(); fail("Exceptie tijdens HSQL database startup."); } try { log.info("Configureer de entityManager"); // N.B.: De entity manager moet testPU heten in de test persistence.xml configuratie emFactory = Persistence.createEntityManagerFactory("testPU"); entityManager = emFactory.createEntityManager(); } catch (Exception ex) { ex.printStackTrace(); fail("Exceptie tijdens JPA EntityManager instantiatie."); } ((EmployeeServiceImpl) employeeService).setEntityManager(entityManager); ((EmployeeServiceImpl) mailService).setMailService(mailService); }
En in de teardown sluiten we de database weer.
@Override protected void tearDown() throws Exception { super.tearDown(); log.info("Shuting down Hibernate JPA layer."); if (entityManager != null) { entityManager.close(); } if (emFactory != null) { emFactory.close(); } log.info("Stop de in-memory HSQL database."); try { connection.createStatement().execute("SHUTDOWN"); } catch (Exception ex) { log.error("error", ex); } }
Om deze JUnit test te laten werken, zal er een persistence.xml file moeten worden gedefinieerd. In dit voorbeeld configureren we Hibernate als persistence provider waarbij HSQL als dialect wordt gekozen:
<?xml version="1.0" encoding="UTF-8"?> <!-- Persistence deployment descriptor for junit testing --> <persistence 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_1_0.xsd" version="1.0"> <persistence-unit name="testPU" transaction-type="RESOURCE_LOCAL"> <provider>org.hibernate.ejb.HibernatePersistence</provider> <class>com.finalist.example.Employee</class> <properties> <property name="hibernate.connection.url" value="jdbc:hsqldb:mem:unit-testing-jpa"/> <property name="hibernate.connection.driver_class" value="org.hsqldb.jdbcDriver"/> <property name="hibernate.dialect" value="org.hibernate.dialect.HSQLDialect"/> <property name="hibernate.hbm2ddl.auto" value="create-drop"/> <property name="hibernate.connection.username" value="sa"/> <property name="hibernate.connection.password" value=""/> </properties> </persistence-unit> </persistence>
Bij het uitvoeren van de JUnit testen zal de persistence.xml in het classpath gevonden moeten worden in de META-INF directory.
Junit testen van Session EJBs
Met de voorgaande basis class is de EntityManager beschikbaar voor het schrijven van testen. Daarnaast kunnen de sessie beans ook prima worden getest met Junit testen door ze op de “Spring” manier op te zetten.
Dit betekent dat elke te testen sessie EJB een publieke set methodes krijgt voor alle afhankelijke services zoals andere sessie EJB’s en de entityManager. Dit is de enige concessie die in de code moet worden gedaan om de Junit testen goed te kunnen uitvoeren.
Zie het volgende voorbeeld waarin een stateless sessie ejb wordt geïnjecteerd met een EmployeeService en een entity manager:
@Stateless public class EmployeeServiceImpl implements EmployeeService { private EntityManager entityManager; private mailService mailService; /** * Injecteer de entityManger. */ // N.B.: De unitName is dus anders dan de unitName van de test persistence.xml. // Hier specificeer je de unitName zoals @PersistenceContext(unitName = "jpaUnit") public void setEntityManager(EntityManager entityManager) { this.entityManager = entityManager; } /** * Injecteer de mail service sessie ejb * * @param mailService implementation. */ @EJB public void setMailService(MailService mailService) { this.mailService = mailService; }
Junit testen vs Integratie testen
Aangezien we het hier hebben over Junit testen zijn het testen die op zich zelf staan en onafhankelijk van externe systemen draaien (zoals een testdatabase) en zijn deze testen dus geen vervanging voor integratie testen.
Een aantal zaken die bijvoorbeeld NIET naar voren komen bij deze testen zijn:
- Injecteren van componenten door de container, zoals de entityManager;
- Database specifieke issues zoals rechten, foreign key constraints, not null velden etc;
- Transactieafhandeling: alle transactie configuratie wordt door de container afgehandeld en wordt dus niet door de Junit testen afgehandeld. In de Junit testen zal in sommige gevallen dan ook expliciet een transactie moeten worden gestart en gestopt.
Conclusie
De voorgestelde opzet van Junit testen van EJB3 code maakt het mogelijk om een hoge code coverage te bereiken van EJB3 code. Zowel voor Entities, JPA queries en Sessie EJBs. Het dwingt wel een programmeerstijl af die is gebaseerd op het springframework. Bijkomend voordeel is dat een mogelijk migratie naar Spring hierdoor eenvoudig is uit te voeren. Naast deze Junit testen blijven integratie testen nog steeds noodzakelijk.

Brr… migratie naar Spring een voordeel?
Ik wordt niet zo blij van code aanpassen aan een testframework.
In dit geval breekt het encapsulation, er komt altijd iemand in de toekomst die zo’n publieke set-method ook echt gaat gebruiken in productiecode.
Alternatief voor het public maken van de entitymanager en andere private members is een wat reflection magie: (ik hoop dat de source leesbaar is). En nu maar hopen dat dit niet teveel in productiecode gebruikt gaat worden
public class TestService {
private String entityManager;
public void sayHello() {
System.out.println(“Entitymanager = “+entityManager);
}
}
en de aanroep
import java.lang.reflect.Field;
public class TestMain {
public static void main(String arg[]) {
TestService service = new TestService();
try {
Field field = TestService.class.getDeclaredField(“entityManager”);
field.setAccessible(true);
field.set( service, “Tada”);
service.sayHello();
} catch (Exception e) {
e.printStackTrace();
}
}
}
Edwin van der Elst May 25, 2009 21:53
De noodzaak tot het aanpassen van de code voor het test framework kan ook worden opgelost door een ander testframework te kiezen, of door het testframework te gebruiken in combinatie met een partial mocking tool die ook private members,methodes en constructors mockt.
Dit scheelt het zelf schrijven van reflection code.
Er zijn voldoende partial mocking tools in omloop, bijvoorbeeld:
JMockit: https://jmockit.dev.java.net/
Powermock: http://code.google.com/p/powermock/
JEasyTest: https://jeasytest.dev.java.net/
MockInject: https://mockinject.dev.java.net/
Ton Swieb May 26, 2009 10:21
@Edwin
Op zich is het aanpassen van de code om het te kunnen testen wel een offer, maar een offer dat uiteindelijk veel oplevert.
De kans dat de publieke methode oneigenlijk wordt gebruikt is volgens mij op te lossen door duidelijk in de Javadoc aan te geven dat deze bedoeld is om te testen en eventueel zou je in de set methode een LOG.warn() bericht kunnen plaatsen waarmee je hierop wordt geattendeerd.
En het zou in dit geval beter zijn om de methode package visibility te geven.
En op zich denk ik dat de coding stijl die je toepast met Spring (of een ander Dependency Injection framework) een grote plus is. Hierdoor is het snel duidelijk welke dependencies je hebt en voorkom je high coupling in je code. En door consequent interfaces te injecten is het testen van de code eenvoudig en het toepassen van alternatieve implementaties eenvoudiger.
Rudie Ekkelenkamp May 26, 2009 11:12
@Ton
Gelukkig is er met Mocking veel mogelijk en als je te maken hebt met code die je niet kan/mag aanpassen is een framework als JMockit erg krachtig om de dependencties uit te mocken.
Maar als je de mogelijkheid hebt, zou ik toch voor de voorgestelde methode gaan die gebaseerd is op dependency injection.
Rudie Ekkelenkamp May 26, 2009 11:15
@Rudie
Ik ben het hier toch niet helemaal mee eens.
Privates via publieke setters beschikbaar maken, alleen maar om te kunnen testen vind ik niet fraai. Het breekt encapsulation (dit was/is toch een van de voordelen van OO?).
Dit staat los van het gebruiken van interfaces ipv implementaties (dat is uiteraard nog steeds een goed idee, maar dat staat hier los van).
Een log.warn() is wel handig, maar ik zou nog strenger zijn (iets als ‘throw exception when not running in testmodus’, of System.exit() ). Maar dan passen we de code weer aan voor test-doeleinden, en daar was ik al niet voor
Edwin van der Elst May 26, 2009 11:38
Op zich valt het breken van encapsulation nog wel mee. Je maakt geen getter beschikbaar en je komt niets te weten over de implementatie details van de geinjecteerde services. De setter heeft dan ook een interface als parameter, geen implementatie.
Maar het is inderdaad suboptimaal.
Een alternatief zou het gebruik van een constructor kunnen zijn waarin je alle services meegeeft. Voordeel is dan wel dat de services immuatable worden en niet per ongeluk worden overschreven met een setter.
Rudie Ekkelenkamp May 26, 2009 12:07
Wanneer JBoss AS je target platform is kun je ook de JBoss Microcontainer eens proberen voor dit soort dingen.
Wat ik ervan snap is dat je dit ding vanuit een JUnit test kan bootstrappen. Dan kun je vervolgens daarbinnen de services en beans deployen die voor je test nodig zijn, en de test uitvoeren.
Doordat je zo JBoss heel minimaal opstart kan het allemaal binnen enkele seconden draaien, precies wat je wil voor je unit tests. Er is ook een specifieke support om het ding vanuit een junit test op te starten (org.jboss.test.kernel.junit.MicrocontainerTest).
Die Microcontainer is er al een tijdje maar totnutoe nogal low profile en zeer matigjes gedocumenteerd. Vanaf JBoss 5 is er meer aandacht voor, zie deze site: http://www.jboss.org/jbossmc/.
Peter Reitsma May 29, 2009 10:34
Zoals je al aangeeft ben je op deze manier zelf verantwoordelijk voor het transactiebeheer. Je zal ook een 2de persistence.xml moeten bijhouden voor deployment omdat de optie “transaction-type=”RESOURCE_LOCAL”" juist weer niet geschikt is voor een EJB container.
Naast de EntityManager is een EJB vaak meer dan een data access object dat alleen maar gegevens ophaalt en wegschrijft. Wil je ander soortige methodes testen of maak je gebruik van EJBContext (of andere zaken) zul je een mockup moeten gebruiken. Zelf maak ik regelmatig gebruik van OpenEJB in testcases, dan heb je een volledige EJB container tot je beschikking. Dit gaat misschien al snel lijken op een integratie test maar kan wel hulpvol zijn. Op de website openejb.apache.org zijn voorbeelden te vinden.
Chris Wesdorp July 10, 2009 11:36
Mooie post!
Trouwens, in plaats van een setEntityManager() methode aan je EJB/DAO toe te moeten voeten, zou je ook zelf de EntityManager kunnen “injecteren” in je testclasses, bij voorbeeld met de onderstaande methodes.
(Dit is min of meer hoe het trouwens ook gedaan word in de container/via de annotaties)
public static boolean setEntityManager(Object daoBean, EntityManager entityManager) {
boolean fieldSet = true;
// Retrieve field
Class objectClass = daoBean.getClass().getSuperclass();
Field emField = getField(objectClass, entityManagerfieldName);
if( emField == null ) {
objectClass = daoBean.getClass();
emField = getField(objectClass, entityManagerfieldName);
}
// Set field
if( emField != null ) {
setField(emField, daoBean, entityManager);
}
else {
fieldSet = false;
logger.warn(entityManagerfieldName + " field is null!" );
}
return fieldSet;
}
private static Field getField(Class objectClass, String fieldName) {
Field field = null;
try {
field = objectClass.getDeclaredField(fieldName);
}
catch(Exception e ) {
logger.warn("Unable to retrieve '" + fieldName + "' field on " + objectClass.getSimpleName()
+ ": [" + e.getClass().getSimpleName() + ": " + e.getMessage() + "]");
}
return field;
}
private static boolean setField(Field field, Object fieldObject, Object fieldValue) {
boolean fieldSet = false;
field.setAccessible(true);
try {
field.set(fieldObject, fieldValue);
fieldSet = true;
}
catch(Exception e) {
logger.warn("Unable to set field '" + field.getName() + "' on " + fieldObject.getClass().getSimpleName()
+ ": [" + e.getClass().getSimpleName() + ": " + e.getMessage() + "]");
}
return fieldSet;
}
Marco Rietveld August 13, 2010 11:12