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>

Advertisements

Vastaa

Täytä tietosi alle tai klikkaa kuvaketta kirjautuaksesi sisään:

WordPress.com-logo

Olet kommentoimassa WordPress.com -tilin nimissä. Log Out / Muuta )

Twitter-kuva

Olet kommentoimassa Twitter -tilin nimissä. Log Out / Muuta )

Facebook-kuva

Olet kommentoimassa Facebook -tilin nimissä. Log Out / Muuta )

Google+ photo

Olet kommentoimassa Google+ -tilin nimissä. Log Out / Muuta )

Muodostetaan yhteyttä palveluun %s