EJB 3 Yksikkötestauksesta 3/3

Artikkelisarjan kahdessa ensimmäisessä osassa käsiteltiin EJB 3 ja JUnit yhteiskäyttöä. Todettiin että on pääosin kolme strategiaa millä EJB komponentteja voi testata:

1) Testataan niitä EJB säiliössä. Testikoodi toimii tavallaan clientin roolissa ja kutsuu ejb fasadin palveluita. Säiliö toimittaa tietokantaresurssit, transaktiot ja muut. Tämä ei ole kovin aitoa yksikkötestausta tai varsinkaan TDD:tä mutta voi olla hyödyllistä testausta silti.

2) Testataan EJB 3 komponentteja ilman säiliötä, tavallisina java-luokkina. Tämä on helppoa triviaaleilla EJB komponenteilla mutta koska niiden logiikka usein kaipaa taustalleen resursseja kuten JNDI, Tietokantapalvelimet, Connection Poolit, transaktiopalvelimet, sanomapalvelimet, JCA connectorit, jne, niitä voi olla vaikeaa testata eristyksessä. Ratkaisuna on pystyttää testaukseen dedikoitu muistitietokanta tms resurssi, joka antaa mahdollisuuden testata businesslogiikan ja pysyvyysratkaisun yhteistoimintaa. Tämä vaatii sen, että kytkös kantaan tai sanomapalvelimeen on helppo vaihtaa, esim. Dependency Injection patternilla. Tämäkään ei ole aivan puhdasta yksikkötestausta mutta voi olla höydyllistä testausta ja on ilman muuta tehtävissä.

3) Puhtaan Test Driven lähestymistavan puitteissa haluamme keskittyä testaamaan EJB:n varsinaista business logiikkaa, ei kannan toimintaa. Lisäksi testien pitäisi olla vakioituja olosuhteiltaan, ja salamannopeita suorittaa (jotta niitä voidaan ajaa toistuvasti ja jatkuvasti, kenen hyvänsä työasemassa). Tämä on mahdollista jos voimme korvata taustaresurssit sopivalla Mock frameworkillä. Tässä mallissa on varjopuolena että Mock objektit pitää ensin ’kouluttaa’ palauttamaan oikeat, odotetut arvot. Toisaalta tämä koulutusvaihe samalla speksaa mitä odotamme sovelluksen tekevän, eli palvelee dokumentoinnin tehtävää.

Tässä artikkelissa esittelen suositun Mock frameworkin Mockiton käyttöä. Testauksen kohteena on jo sarjan 2. osassa esitelty BankFacade SessionBean, joka näyttää siis tältä:

@Stateless
public class BankFacadeBean implements BankFacadeLocal {

@PersistenceContext
private EntityManager em;

public EntityManager getEm() {
return em;
}

public void setEm(EntityManager em) {
this.em = em;
}

public void updateAccount(Account a) {
em.merge(a);
}

public void createAccount(Account a) {
em.persist(a);
}

public Account getAccount(Long id) {
return em.find(Account.class, id);
}

}

Yksinkertainen Bean, jolla tulee ajon aikana olemaan EntityManager injektoituna jäsenmuuttujaan. Meidän testimme kannalta sellaista ei kuitenkaan ole – joten mockaamme sellaisen. Tässä on Mockito frameworkiä käyttävän JUnit yksikkötestin luokkamääritys ja setUp-metodi:

… snip …

import static org.mockito.Mockito.*;

public class BankFacadeBeanMockTest extends TestCase {

private static Logger logger = Logger.getLogger(BankFacadeBeanMockTest.class.getName());
private EntityManager em;

public static final Account ACCOUNT = new Account();
public static final double BALANCE=10000.0;

public BankFacadeBeanMockTest(String testName) {
super(testName);
}
private BankFacadeBean instance;

@Override
protected void setUp() throws Exception {
super.setUp();

ACCOUNT.setBalance(BALANCE); // initialize account
ACCOUNT.setId(Long.valueOf(1));

// Set up a MOCK EntityManager here
em = mock(EntityManager.class);

// teach mock how to do its things

// make sure ACCOUNT is in EntityManager context
when(em.contains(ACCOUNT)).thenReturn(Boolean.TRUE);

// make sure we can find account
when(em.find(Account.class, Long.valueOf(1))).thenReturn(ACCOUNT);

instance = new BankFacadeBean();
instance.setEm(em);
}

.. snip …

Kävikö selväksi? Importoimme staattisen Mockito objektin kaikki metodit. Loimme testiä varten vakioidun tilin jossa tilinumero ja saldo pysyy samana (refaktoroinnille olisi kyllä jo käyttöä..). mock()-metodilla loimme EntityManager rajapinnasta mock objektin. Mock objektin kolmea metodia tullaan testeissä kutsumaan, joten varmistamme että ne palauttavat odotetut arvot:

1) contains() palauttaa true oikealle tilille – ja väittää näin objektin löytyvän kannasta

2) find() palauttaa luomamme tilin – aivan kuin se olisi haettu kannasta.

Lopuksi injektoimme mockatun EntityManager objektin pankki-fasadille, ja alamme testailemaan sitä. Tässä testirutiini:

public void testPersistence() {
System.out.println(”updateAccount”);

try {
// find existing account from db

// update account data

//a.setId(Long.valueOf(1000)); // don’t set id, since it’s autoid

instance.createAccount(ACCOUNT);
Long id = ACCOUNT.getId();
System.out.println(”Created with account id ”+ id);

// em should know Account a now
assertTrue(em.contains(ACCOUNT));
// Verify that our account exists and has proper balance
Account b = instance.getAccount(id);
assertEquals(BALANCE,b.getBalance());
} catch (Exception ex) {
ex.printStackTrace();
fail(”Exception during testPersistence”);
}
}

Mockito Unit Test

Mockito yksikkötestaus EJB 3.0 komponenteille on helppoa ja salamannopeaa

Testi ei siis vaadi EJB säiliötä, tai tietokantaa, tai muutakaan resurssia, testaamme Java-luokkaa pelkkänä POJO:na. Tällaista testiä voit ajaa salamannopeasti uudelleen, ja uudelleen, ja uudelleen aina muutoksia tehtäessä. Se ei mittaa tietokannan tai muiden taustajärjestelmien toimintaa, mutta varmentaa ja dokumentoi mitä odotamme niiden osien tekevän. Huomaa että tämä tekee refaktoroinnista varmempaa ja auttaa myös sovelluksen suunnittelussa.

Jos tämä herätti kiinnostuksen, kannattaa seurata myös dirty-mockito projektia (http://code.google.com/p/dirty-mockito/wiki/JpaSupport), tässä maistiainen siitä:

public class WidgetDaoTest extends ActiveTest<WidgetDao> {

@Mock
private EntityManager em;

@Mock
private Query query;

private WidgetDao widgetDao;

@Test
public void testFindByName() {
Widget widget = new Widget();

when(em.createNamedQuery(”WidgetDao.findByName”)).thenReturn(query);
when(query.getResultList()).thenReturn(Collections.singletonList(widget));

assertSame(widget, widgetDao.findByName(”Bob”));

verify(query).setParameter(”name”, ”Bob”);
}

}

Jos olet utelias, Maven pom.xml tiedosto tälle projektille näyttää tältä:

<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>fi.tieturi</groupId>
<artifactId>EJB3UnitTestDemo</artifactId>
<packaging>ejb</packaging>
<version>1.0-SNAPSHOT</version>
<name>EJB3MockUnitTestDemo JEE5 EJB (ejb)</name>
<url>http://maven.apache.org</url&gt;
<dependencies>

<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>3.8.1</version>
<scope>test</scope>
</dependency>

<!– NOTE: This was needed as fix for Absent Code error that you get –>
<!– Put this dependency BEFORE javaee apis so you have persistence implementation available –>
<dependency>
<groupId>net.java.dev.glassfish</groupId>
<artifactId>glassfish-persistence-api</artifactId>
<version>b32g</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>javaee</groupId>
<artifactId>javaee-api</artifactId>
<version>5</version>
<scope>provided</scope>
</dependency>

<!– for Mockito mock framework –>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-all</artifactId>
<version>1.8.2</version>
<scope>test</scope>
</dependency>

</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>2.0.2</version>
<configuration>
<source>1.5</source>
<target>1.5</target>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-ejb-plugin</artifactId>
<version>2.1</version>
<configuration>
<ejbVersion>3.0</ejbVersion>
</configuration>
</plugin>

</plugins>
<finalName>EJB3UnitTestDemo</finalName>
</build>
</project>

Mainokset

EJB 3 Yksikkötestauksesta 2/3

Edellisessä artikkelisarjan osassa tarkasteltiin Javan uutta EJB 3 business komponenttia ja todettiin että sitä pystyy nyt testaamaan suoraan JUnitilla, ilman EJB säiliötä. Ongelmana on kuitenkin se, että aidon elämän EJB komponentit eivät vain laske lukuja yhteen vaan integroivat dataa esim. tietokannoista, sanomapalveluista, jne. Toisin sanoen niiden realistinen testaus edellyttää erilaisten tietolähteiden olemassaoloa – muuten testiajo tuottaa vain NullPointerException:eitä.

Otetaanpa esimerkiksi yksinkertainen EJB Session Bean joka käyttää EntityManager rajapintaa tietokantaoperaatioihin:

@Stateless
public class BankFacadeBean implements BankFacadeLocal {

@PersistenceContext
private EntityManager em;

public EntityManager getEm() {
return em;
}

public void setEm(EntityManager em) {
this.em = em;
}

public void updateAccount(Account a) {
em.merge(a);
}

public void createAccount(Account a) {
em.persist(a);
}

public Account getAccount(Long id) {
return em.find(Account.class, id);
}

}

Tässä on kaksi tapaa lähestyä: Täysin puhdasoppinen tapa on Mock objektien käyttö, mutta se vaatii tietysti mock objektien virittelyn toimivaksi. Tässä on etuna nopeus ja puhdas TDD lähestymistapa. Toinen vaihtoehto on pystyttää testikanta, erillään aidosta, ja tehdä esim. kantaoperaatiot sinne. Tässä on etuna kokonaislogiikan testaus, mutta lähestymistapa ei ole kovin puhdas eikä nopea, ja kannan pystyttäminenkin vaatii ekstratyötä testiympäristössä. Toisaalta se mittaa muutakin kuin logiikkaa, ja antaa realistisen kuvan siitä miten business metodit toimivat kokonaisuudessaan.

Oman EntityManager ratkaisun tekeminen on helppoa, sillä voit tehdä oman persistence unit määrityksen testimoduuliin, ja ladata sen käyttäen Standard Edition mallia – täysin ilman JNDI:tä tai EJB säiliötä. Voit injektoida sen beanillesi (tarvitset get/set mekanismin tai muun tavan injektoida) – ja pystyt testaamaan logiikkaa. Samaa menetelmää voi soveltaa muillekin resursseille, mutta tarvitset tietysti edelleen taustalle vastaavat ajurit, kantapalvelimen, sanomapalvelimen, jne… Muista kuitenkin että jos EJB säiliö ei olekaan välissä, myöskään container-managed transaction, security, jne piirteet eivät toimi. Joten joudut hallinnoimaan myös transaktiot käsin.

Jotta testauksesta saa edes kohteellisen nopeaa, on kätevintä pystyttää muistinvarainen testitietokanta vain testauksen tarpeisiin. Tämä onnistuu helposti käyttämällä esim. JDK 6:sta alkaen sisäänrakennettua Derby-kantaa. Tässä esimerkki testikoodista joka testaa EJB 3.0 session facade komponenttia joka vuorostaan käyttää JPA rajapinnan operaatioita tilitietojen tallennukseen ja lataukseen. Ensiksi setUp ja tearDown metodit:

… snip …

public class BankFacadeBeanTest extends TestCase {

private static Logger logger = Logger.getLogger(BankFacadeBeanTest.class.getName());
private EntityManagerFactory emFactory;
private EntityManager em;

public static final double BALANCE=10000.0;

public BankFacadeBeanTest(String testName) {
super(testName);
}
private BankFacadeBean instance;

@Override
protected void setUp() throws Exception {

super.setUp();
try {
logger.info(”Starting in-memory database for unit tests”);
Class.forName(”org.apache.derby.jdbc.EmbeddedDriver”);
DriverManager.getConnection(”jdbc:derby:memory:unit-testing-jpa;create=true”).close();
} catch (Exception ex) {
ex.printStackTrace();
fail(”Exception during database startup.”);
}

try {
logger.info(”Building JPA EntityManager for unit tests”);
emFactory = Persistence.createEntityManagerFactory(”testPU”);
em = emFactory.createEntityManager();
} catch (Exception ex) {
ex.printStackTrace();
fail(”Exception during JPA EntityManager instanciation.”);
}

instance = new BankFacadeBean();
instance.setEm(em);
}

@Override
protected void tearDown() throws Exception {
super.tearDown();
logger.info(”Shuting down Hibernate JPA layer.”);
if (em != null) {
em.close();
}
if (emFactory != null) {
emFactory.close();
}
logger.info(”Stopping in-memory database.”);
try {
DriverManager.getConnection(”jdbc:derby:memory:unit-testing-jpa;shutdown=true”).close();
} catch (SQLNonTransientConnectionException ex) {
if (ex.getErrorCode() != 45000) {
throw ex;
}
// Shutdown success
}
VFMemoryStorageFactory.purgeDatabase(new File(”unit-testing-jpa”).getCanonicalPath());

}

Yksinkertaista? Eli ennen testiä pystytetään muistinvarainen tietokanta johon voi luoda vapaasti mitä rakenteita haluaa Entity Objecteja varten. Testin jälkeen tearDown metodi käy poistamassa tilapäiskannan ja palataan alkutilaan.

Itse testimetodista on vaikeaa tehdä kovin hienojyväistä, koska yksikkötestimetodit voidaan ajaa riippumatta toisistaan eri järjestyksissä. Voisi olla hyvä idea luoda vähän mallidataa setup metodissa kantaan, mutta yksi vaihtoehto on tehdä se aina testimetodissa. Oikeaoppisesti olisi syytä poistaa testidata mutta tearDown poistaa koko kannan joten siihen ei ole tarvetta. Tässä esimerkki BankFacade komponentin testauksesta:

/**
* Test of updateAccount method, of class BankFacadeBean.
*/
public void testPersistence() {
System.out.println(”updateAccount”);

try {
em.getTransaction().begin();

// find existing account from db
// update account data
Account a = new Account();
//a.setId(Long.valueOf(1000)); // don’t set id, since it’s autoid
a.setBalance(BALANCE);
instance.createAccount(a);
Long id = a.getId();
System.out.println(”Created with account id ”+ id);

// em should know Account a now
assertTrue(em.contains(a));
em.getTransaction().commit();
em.getTransaction().begin();
// Verify that our account exists and has proper balance
Account b = instance.getAccount(id);
assertEquals(BALANCE,b.getBalance());
em.getTransaction().commit();

} catch (Exception ex) {
em.getTransaction().rollback();
ex.printStackTrace();
fail(”Exception during testPersistence”);
}
}

Miten sitten pystytät testikannan? Tässä on JPA:n käyttämä persistence.xml joka kuvaa testikantayhteyden (Maven projekteissa sijoitetaan src/main/resources/META-INF kansioon):

<?xml version=”1.0″ encoding=”UTF-8″?>
<persistence version=”1.0″ xmlns=”http://java.sun.com/xml/ns/persistence”>
<persistence-unit name=”testPU”
transaction-type=”RESOURCE_LOCAL”>
<provider>org.hibernate.ejb.HibernatePersistence</provider>
<class>bank.Account</class>
<exclude-unlisted-classes>true</exclude-unlisted-classes>
<properties>
<property name=”hibernate.connection.url” value=”jdbc:derby:memory:unit-testing-jpa”/>
<property name=”hibernate.connection.driver_class” value=”org.apache.derby.jdbc.EmbeddedDriver”/>
<property name=”hibernate.dialect” value=”org.hibernate.dialect.DerbyDialect”/>
<property name=”hibernate.hbm2ddl.auto” value=”create”/>
<property name=”hibernate.connection.username” value=””/>
<property name=”hibernate.connection.password” value=””/>
</properties>
</persistence-unit>
</persistence>

Siinä se! Tässä artikkeliosassa siis näytettiin miten helposti voit testata EJB 3 komponentteja ilman aitoa EJB säiliötä JUnit yksikkötestien avulla. Voit simuloida kantaresurssia salamannopealla muistikannalla joka myös palautuu alkutilaan käytön jälkeen eli tarjoaa vakioidun testiympäristön. Tämä ei ole välttämättä aitoa Test Driven kehitystä eikä puhdasoppista yksikkötestausta mutta jos haluat nimenomaan testata kantaoperaatioiden toimintaa kokonaisuutena eikä vain logiikkaa, tämä lähestymistapa tarjoaa siihen keinot.

Seuraavassa artikkelisarjan osassa puhutaan Mock objektien käytöstä testauksessa, mikä mahdollistaa puhtaan logiikan testauksen ilman taustaresurssien pystyttämistä.

————————————————————————————————–

Tämä testi on rakennettu hyödyntäen Maven alustaa riippuvuuksien ja kirjastojen suhteen, tässä on pom.xml tiedosto projektille:
<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>fi.tieturi</groupId>
<artifactId>EJB3UnitTestDemo</artifactId>
<packaging>ejb</packaging>
<version>1.0-SNAPSHOT</version>
<name>EJB3UnitTestDemo JEE5 EJB</name>
<url>http://maven.apache.org</url&gt;
<dependencies>

<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>3.8.1</version>
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate</artifactId>
<version>3.2.5.ga</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>ejb3-persistence</artifactId>
<version>1.0.1.GA</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-entitymanager</artifactId>
<version>3.3.2.GA</version>
<scope>test</scope>
</dependency>
<!– this is needed for built-in java derby db –>
<dependency>
<groupId>org.apache.derby</groupId>
<artifactId>derby</artifactId>
<version>10.5.3.0_1</version>
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.apache.derby</groupId>
<artifactId>derbyclient</artifactId>
<version>10.5.3.0_1</version>
<scope>test</scope>
</dependency>

<!– NOTE: This was needed as fix for Absent Code error that you get –>
<!– Put this dependency BEFORE javaee apis so you have persistence implementation available –>
<dependency>
<groupId>net.java.dev.glassfish</groupId>
<artifactId>glassfish-persistence-api</artifactId>
<version>b32g</version>
<scope>test</scope>
</dependency>

<dependency>
<groupId>javaee</groupId>
<artifactId>javaee-api</artifactId>
<version>5</version>
<scope>provided</scope>
</dependency>

</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>2.0.2</version>
<configuration>
<source>1.5</source>
<target>1.5</target>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-ejb-plugin</artifactId>
<version>2.1</version>
<configuration>
<ejbVersion>3.0</ejbVersion>
</configuration>
</plugin>

</plugins>
<finalName>EJB3UnitTestDemo</finalName>
</build>
</project>