Unit testing is een belangrijk onderdeel van Agile software ontwikkeling. Vooral de grotere projecten met afgeschermde componenten lenen zich uitstekend voor unit testing.
Het idee achter Unit Testing is dat deze individuele componenten in een zgn. Test Case getest worden, door de publieke methoden binnen deze componenten met bepaalde argumenten aan te roepen, en te controlleren of het resultaat correct is. Zie ook dit wikipedia artikel.
Unit testing klinkt altijd als een erg aantrekkelijke manier om een groot gedeelte van je testprocess te automatiseren. Maar bij de implementatie van Unit Tests loop je al snel tegen problemen aan.
Utility classes zijn makkelijk te testen met JUnit. Deze classes hebben immers weinig afhankelijkheden met andere onderdelen van je applicatie. Het wordt echter lastig op het moment dat je een EJB applicatie aan het ontwikkelen bent; veel classes binnen je applicatie kunnen alleen binnen een EJB container draaien (zoals JBoss). Een goed voorbeeld hiervan is de EntityManager, die je in je Session Beans gebruikt om entiteiten op te halen uit een database. De EntityManager is implementatie specifiek, en vereist dat de applicatie binnen een container draait.
Voorbeeld
Ik geef een klein rudimentair voorbeeld van een Session Bean:
@Stateless
@LocalBinding(jndiBinding="RegistrationManager")
public class RegistrationManagerBean implements RegistrationManager {
@PersistenceContext(unitName="MyPersistenceUnit")
private EntityManager em;
public boolean registerEmailWithPassword(String email, String password) {
User user = new User();
user.setEmail(email);
user.setPassword(password);
em.persist(user);
}
public boolean isEmailRegistered(String email) {
Query query = em.createNamedQuery("User.findUserByEmail");
query.setParameter("email", email);
query.setFirstResult(0);
query.setMaxResults(1);
List<User> list = query.getResultList();
if(list.isEmpty()) {
return false;
}
return true;
}
}
Bovenstaande class heeft 2 publieke functies: het registreren van een nieuwe e-mail adres, en het checken of een bepaald e-mail adres al is geregistreerd.
JUnit test
Een simpele JUnit test voor deze 2 functies zou er als volgt uit kunnen zien:
public class SomeSimpleTest {
@Test
public void testRegisterNewUser() {
// Ja, alsof dit gaat werken ;-)
RegistrationManagerBean rm = new RegistrationManagerBean();
boolean result = rm.registerEmailWithPassword("test@test.nl", "test123");
}
@Test
public void testRegistered {
RegistrationManagerBean rm = new RegistrationManagerBean();
if( !rm. isEmailRegistered("test@test.nl") ) {
fail("Oh no! Missing user!");
}
}
}
Zoals je misschien zult verwachten zal deze test case niet werken. De session bean wordt direct geinstantieerd, zonder hem via zijn interface te benaderen. Alle EJB injections zullen niet plaatsvinden, dus de EntityManager is niet beschikbaar.
De enige manier om deze Session bean correct te testen is door de tests in een EJB container te draaien. Maar dat zou betekenen dat je voor elke JUnit test een JBoss server moet starten. Het starten van JBoss duurt lang, dus zo’n situatie is verre van ideaal.
Wat je eigenlijk wilt is op een test knop drukken in Eclipse, die je hele project voor je test. En dat kan, met OpenEJB. OpenEJB is een lichtgewicht EJB container, start binnen 10 seconden op, en blijft leven gedurende je test cases. Om die JUnit tests vanuit Eclipse uit te kunnen voeren, moet je wel eerst even OpenEJB in Eclipse integreren. Ik ga hier verder niet in op de details, Google kan je hier meer over vertellen.
Zodra je JUnit en OpenEJB in Eclipse hebt gehangen, kun je JUnit tests starten. Maar daarmee zijn we er nog niet. Het volgende probleem is de Persistency laag.
Persistency?
Een methode die we bij Ambrero veel gebruiken is het developen tegen een centrale development database. Op kantoor staat een server met een MySQL installatie, waar alle databases draaien voor onze projecten. Vaak worden deze databases ook gebruikt binnen EJB projecten.
Bij het draaien van bovenstaande JUnit test wordt er een nieuwe gebruiker weggeschreven in de database. Dat zou echter betekenen dat er voor elke JUnit run vervuilende records in de database terecht komen. Je zou dit op kunnen lossen door de database na het draaien van de tests weer op te schonen, maar los daarvan is het erg onhandig om een ontwikkeldatabase te vervuilen met testrecords.
Hier zijn 2 oplossingen voor:
- Het gebruik van een In Memory database
- Het gebruik van een aparte Test MySQL database
Idealiter gebruik je niet geen vendor specifieke SQL functionaliteiten, maar de meeste In Memory databases (zoals Derby en h2) zijn erg gelimiteerd qua datatypen en functies. Ik loop toch vaak tegen het probleem aan dat ik dusdanig veel MySQL specifieke dingen doe, dat het onmogelijk is om de tests op een Memory database uit te voeren (een embedded MySQL database, dat zou pas een oplossing zijn!).
Los van de oplossing die je kiest, zul je voordat je gaat testen je applicatie moeten vertellen dat hij niet met de gewoonlijke database moet communiceren, maar met een specifieke test database. Hieronder laat ik een Test Case zien die de Session beans via de container opzoekt (ipv zelf instantieren) en de juiste database gebruikt.
public class SomeSimpleTest {
private static InitialContext initialContext;
private static RegistrationManager rm;
@BeforeClass
public static void setUpBeforeClass() throws Exception {
Properties p = new Properties();
// Voeg nieuwe persistency informatie toe.
p.put("myDatabase", "new://Resource?type=DataSource");
p.put("myDatabase.JdbcDriver", com.mysql.jdbc.Driver");
p.put("myDatabase.JdbcUrl", mysql://testserver:3306/MyProject");
p.put("myDatabase.JdbcUser", test");
p.put("myDatabase.JdbcPass", "test123!");
initialContext = new InitialContext(p);
// Zoek de RegistrationManager op d.m.v. de interface
rm = (RegistrationManager)initialContext.lookup("RegistrationManager");
assertNotNull(rm);
}
@Test
public void testRegisterNewUser() {
boolean result = rm.registerEmailWithPassword("test@test.nl", "test123");
}
@Test
public void testRegistered {
if( !rm. isEmailRegistered("test@test.nl") ) {
fail("Oh no! Missing user!");
}
}
}
Bovenstaande test-case regelt een aantal zaken voordat de tests gaan draaien.
Eerst injecteert de test-case nieuwe informatie over myDatabase (de database die gebruikt wordt in de persistency unit ‘MyPersistenceUnit’, waarnaar verwezen wordt in de Session bean) en vervolgens zoekt de test-case de RegistrationManager op in de container.
Voor meer informatie over het injecteren van database informatie, zie openejb.apache.org/3.0/openjpa. Meer lezen over onze favoriete technieken kun je lezen op onze pagina over onze technische expertise.