これは何? †
- Glassfish Session Bean で作った Session Bean をテストする
- jMockit で JavaEE によってインジェクションされるオブジェクトを埋め込む
- jMockit で依存クラスを Mock Object 化する
- dbunit で Excel シートに定義したテストデータをテスト用のデータベース (Derby) に投入する
- dbunit で Excel シートに定義した予測データとデータベース (Derby) の内容を比較する
- 組み込みコンテナ (EJB lite) はテストに使わないの ? → 使いません。起動が遅い。設定が大変
pom.xml †
<?xml version="1.0" encoding="UTF-8"?>
<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/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<artifactId>SampleEar</artifactId>
<groupId>com.mycompany</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<groupId>com.mycompany</groupId>
<artifactId>SampleEar-ejb</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>ejb</packaging>
<name>SampleEar-ejb</name>
<properties>
<endorsed.dir>${project.build.directory}/endorsed</endorsed.dir>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<netbeans.hint.deploy.server>gfv3ee6</netbeans.hint.deploy.server>
<jmockit.version>1.1</jmockit.version>
</properties>
<dependencies>
<dependency>
<groupId>org.eclipse.persistence</groupId>
<artifactId>eclipselink</artifactId>
<version>2.3.2</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.eclipse.persistence</groupId>
<artifactId>javax.persistence</artifactId>
<version>2.0.3</version>
</dependency>
<dependency>
<groupId>org.eclipse.persistence</groupId>
<artifactId>org.eclipse.persistence.jpa.modelgen.processor</artifactId>
<version>2.3.2</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.derby</groupId>
<artifactId>derby</artifactId>
<version>10.9.1.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.googlecode.jmockit</groupId>
<artifactId>jmockit</artifactId>
<version>${jmockit.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.dbunit</groupId>
<artifactId>dbunit</artifactId>
<version>2.4.9</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.5.6</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-nop</artifactId>
<version>1.5.6</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi</artifactId>
<version>3.2-FINAL</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>javax</groupId>
<artifactId>javaee-api</artifactId>
<version>6.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.mycompany</groupId>
<artifactId>SampleEar-common</artifactId>
<version>1.0-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>2.3.2</version>
<configuration>
<source>1.7</source>
<target>1.7</target>
<compilerArguments>
<endorseddirs>${endorsed.dir}</endorseddirs>
</compilerArguments>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-ejb-plugin</artifactId>
<version>2.3</version>
<configuration>
<ejbVersion>3.1</ejbVersion>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<version>2.1</version>
<executions>
<execution>
<phase>validate</phase>
<goals>
<goal>copy</goal>
</goals>
<configuration>
<outputDirectory>${endorsed.dir}</outputDirectory>
<silent>true</silent>
<artifactItems>
<artifactItem>
<groupId>javax</groupId>
<artifactId>javaee-endorsed-api</artifactId>
<version>6.0</version>
<type>jar</type>
</artifactItem>
</artifactItems>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.14</version>
<configuration>
<argLine>
-javaagent:"${settings.localRepository}"/com/googlecode/jmockit/jmockit/${jmockit.version}/jmockit-${jmockit.version}.jar
</argLine>
</configuration>
</plugin>
</plugins>
</build>
<repositories>
<repository>
<url>http://download.eclipse.org/rt/eclipselink/maven.repo/</url>
<id>eclipselink</id>
<layout>default</layout>
<name>Repository for library EclipseLink (JPA 2.0)</name>
</repository>
</repositories>
</project>
テスト対象 †
- OrderService?.java
package com.mycompany.biz;
import com.mycompany.common.BusinessException;
import com.mycompany.entity.CustomerTable;
import com.mycompany.entity.ItemTable;
import com.mycompany.entity.OrderTable;
import java.util.Date;
import javax.ejb.EJB;
import javax.ejb.LocalBean;
import javax.ejb.Stateless;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import org.apache.commons.lang.time.DateUtils;
@Stateless
@LocalBean
public class OrderService {
@PersistenceContext(unitName = "warehousePU")
private EntityManager em;
@EJB
private FactoryService factory;
public OrderTable orderItem(final long customerId, final long itemId, final int amount) throws BusinessException {
CustomerTable customer = em.find(CustomerTable.class, customerId);
ItemTable item = em.find(ItemTable.class, itemId);
OrderTable order = new OrderTable();
order.setCustomerId(customer);
order.setItemId(item);
order.setAmount(amount);
if (item.getStock() > amount) {
// 在庫があった
item.setStock(item.getStock() - amount);
Date shipping = DateUtils.addDays(new Date(), 1);
order.setShipdate(shipping);
} else {
// 在庫がなかったので工場に発注
Date supply = factory.backorder(itemId, amount - item.getStock());
Date shipping = DateUtils.addDays(supply, 1);
order.setShipdate(shipping);
item.setStock(0L);
}
em.persist(order);
return order;
}
}
- 受注 Session Bean
- 「顧客ID」、「物品ID」、「個数」 を引数に呼び出されて、出荷日が翌日の「受注」を作成する。在庫が足りない場合には工場に発注する。
- 実行時には Entity Manager と、別の Session Bean がコンテナからインジェクションされる
- テストをするためには、インジェクションされるオブジェクトを埋め込んでやる必要がある
テストプログラム1 (dbunit) †
package com.mycompany.test.biz;
import com.mycompany.biz.OrderService;
import com.mycompany.entity.OrderTable;
import java.io.File;
import java.io.IOException;
import java.sql.Connection;
import java.sql.SQLException;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.Persistence;
import mockit.Deencapsulation;
import org.dbunit.Assertion;
import org.dbunit.DatabaseUnitException;
import org.dbunit.database.DatabaseConnection;
import org.dbunit.database.IDatabaseConnection;
import org.dbunit.dataset.IDataSet;
import org.dbunit.dataset.ITable;
import org.dbunit.dataset.excel.XlsDataSet;
import org.dbunit.dataset.filter.DefaultColumnFilter;
import org.dbunit.operation.DatabaseOperation;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;
import static org.junit.Assert.assertThat;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.equalTo;
/**
*
* @author atsushi
*/
public class OrderServiceTest {
private static EntityManagerFactory emf;
private static EntityManager em;
private static OrderService target = new OrderService();
@BeforeClass
public static void setUpClass() {
emf = Persistence.createEntityManagerFactory("warehouseTestPU");
em = emf.createEntityManager();
// Injection
Deencapsulation.setField(target, "em", em);
}
@AfterClass
public static void tearDownClass() {
em.close();
emf.close();
}
@Test
public void test() throws Exception {
loadTestDb("src/test/resources/OrderServiceTest_ini.xls");
// ▽ Execute Target
em.getTransaction().begin();
OrderTable order = target.orderItem(1, 1, 20);
em.getTransaction().commit();
// △ Execute Target
assertThat(1L, is(equalTo(order.getCustomerId().getId())));
assertThat(1L, is(equalTo(order.getItemId().getId())));
assertThat(80L, is(equalTo(order.getItemId().getStock())));
assertThat(20L, is(equalTo(order.getAmount())));
IDataSet actualTable = loadActualTable();
IDataSet expectedTable = new XlsDataSet(new File("src/test/resources/OrderServiceTest_exp.xls"));
// Filter shipdate
ITable filteredTable = DefaultColumnFilter.includedColumnsTable(
new SortedTable(
actualTable.getTable("order_table"), new String[]{"id"}),
expectedTable.getTableMetaData("order_table").getColumns());
Assertion.assertEquals(expectedTable.getTable("order_table"), filteredTable);
}
private void loadTestDb(String excel) throws DatabaseUnitException, SQLException, IOException {
em.getTransaction().begin();
Connection jdbc = em.unwrap(java.sql.Connection.class);
IDatabaseConnection conDbUnit = new DatabaseConnection(jdbc);
IDataSet dataSet = new XlsDataSet(new File(excel));
DatabaseOperation.CLEAN_INSERT.execute(conDbUnit,dataSet);
em.getTransaction().commit();
}
private IDataSet loadActualTable() throws DatabaseUnitException, SQLException, IOException {
em.getTransaction().begin();
Connection jdbc = em.unwrap(java.sql.Connection.class);
IDatabaseConnection conDbUnit = new DatabaseConnection(jdbc);
IDataSet dataSet = conDbUnit.createDataSet(new String[]{"item_table", "customer_table", "order_table"});
//XlsDataSet.write(dataSet, new FileOutputStream(new File("/tmp/result.xls")));
em.getTransaction().commit();
return dataSet;
}
}
- 基本戦略
- データベースへのデータの投入
private void loadTestDb(String excel) throws DatabaseUnitException, SQLException, IOException {
em.getTransaction().begin();
Connection jdbc = em.unwrap(java.sql.Connection.class);
IDatabaseConnection conDbUnit = new DatabaseConnection(jdbc);
IDataSet dataSet = new XlsDataSet(new File(excel));
DatabaseOperation.CLEAN_INSERT.execute(conDbUnit,dataSet);
em.getTransaction().commit();
}
- dbunit で Excel からデータを投入する (Excel 97-2003形式)
- データを投入するためには JDBC 接続が必要。Eclipse link (Glassfish) の場合には、EntityManager? から取得できる (公開されている正式な仕様ではない)
- ミソ : RDB が自動的に採番する ID は入れる必要が無い
- ミソ : JavaEE が採番する Version は入れておく必要がある。JPA は、Version 項目が入っているものとして動く
- データの比較
assertThat(1L, is(equalTo(order.getCustomerId().getId())));
assertThat(1L, is(equalTo(order.getItemId().getId())));
assertThat(80L, is(equalTo(order.getItemId().getStock())));
assertThat(20L, is(equalTo(order.getAmount())));
IDataSet actualTable = loadActualTable();
IDataSet expectedTable = new XlsDataSet(new File("src/test/resources/OrderServiceTest_exp.xls"));
// Filter shipdate
ITable filteredTable = DefaultColumnFilter.includedColumnsTable(
actualTable.getTable("order_table"),
expectedTable.getTableMetaData("order_table").getColumns());
Assertion.assertEquals(expectedTable.getTable("order_table"), filteredTable);
private IDataSet loadActualTable() throws DatabaseUnitException, SQLException, IOException {
em.getTransaction().begin();
Connection jdbc = em.unwrap(java.sql.Connection.class);
IDatabaseConnection conDbUnit = new DatabaseConnection(jdbc);
IDataSet dataSet = conDbUnit.createDataSet(new String[]{"item_table", "customer_table", "order_table"});
//XlsDataSet.write(dataSet, new FileOutputStream(new File("/tmp/result.xls")));
em.getTransaction().commit();
return dataSet;
}
- dbunit で読み込んだ Excel とデータベースの内容を比較する (Excel 97-2003形式)
- 比較エラーが起きたら、読み込んだデータベースの内容を /tmp/result.xls に書き出してみる
- 比較したくない項目は DefaultColumnFilter?.includedColumnsTable? で絞り込む。(納入日は実行日の翌日なので、あらかじめ用意したデータとは比較できない)
- 行をソートする場合には、new SortedTable?(tbl, String[]) で、ソート済みのテーブルを作る。RDB では、行の呼び出し順は不定なので、「場合には」というか「必ず」やらないと、Excel で作った期待状態との比較ができない。
テストプログラム2 (jmockit) †
package com.mycompany.test.biz;
import com.mycompany.biz.FactoryService;
import com.mycompany.biz.OrderService;
import com.mycompany.entity.OrderTable;
import java.io.File;
import java.io.IOException;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.Date;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.Persistence;
import mockit.Deencapsulation;
import mockit.Expectations;
import mockit.Mocked;
import org.apache.commons.lang.time.DateUtils;
import org.dbunit.DatabaseUnitException;
import org.dbunit.database.DatabaseConnection;
import org.dbunit.database.IDatabaseConnection;
import org.dbunit.dataset.IDataSet;
import org.dbunit.dataset.excel.XlsDataSet;
import org.dbunit.operation.DatabaseOperation;
import org.junit.After;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
/**
*
* @author atsushi
*/
public class OrderServiceTest2 {
private EntityManagerFactory emf;
private EntityManager em;
private OrderService target = new OrderService();
/**
* MockObject.
* 注意 "private static" 不可
*/
@Mocked
private FactoryService mockFactory;
@Before
public void setUp() {
emf = Persistence.createEntityManagerFactory("warehouseTestPU");
em = emf.createEntityManager();
// Injection
Deencapsulation.setField(target, "em", em);
Deencapsulation.setField(target, "factory", mockFactory);
}
@After
public void tearDown() {
em.close();
emf.close();
}
@Test
public void test() throws Exception {
loadTestDb("src/test/resources/OrderServiceTest_ini.xls");
new Expectations() {{
// mockFactory が 引数 (1L,20) で 1 回呼ばれる事を期待する。
// 返値には、現在日の 2 日後を一律返す
Date mockReturn = DateUtils.addDays(new Date(), 2);
mockFactory.backorder(1L, 20); result=mockReturn; times=1;
}};
// ▽ Execute Target
em.getTransaction().begin();
OrderTable order = target.orderItem(1, 1, 120);
em.getTransaction().commit();
// △ Execute Target
// 検証略
}
private void loadTestDb(String excel) throws DatabaseUnitException, SQLException, IOException {
em.getTransaction().begin();
Connection jdbc = em.unwrap(java.sql.Connection.class);
IDatabaseConnection conDbUnit = new DatabaseConnection(jdbc);
IDataSet dataSet = new XlsDataSet(new File(excel));
DatabaseOperation.CLEAN_INSERT.execute(conDbUnit,dataSet);
em.getTransaction().commit();
}
}
- 基本戦略
- jmockit の使い方
- Unit Test のフィールド変数に @Mock 付きで、mock にしたいオブジェクトを定義
@Mocked
private FactoryService mockFactory;
- @Before で、テスト対象(target)に Injection
Deencapsulation.setField(target, "factory", mockFactory);
- @Test 内で、mockFactory の振る舞いを定義
new Expectations() {{
// mockFactory が 引数 (1L,20) で 1 回呼ばれる事を期待する。
// 返値には、現在日の 2 日後を一律返す
Date mockReturn = DateUtils.addDays(new Date(), 2);
mockFactory.backorder(1L, 20); result=mockReturn; times=1;
}};
- Expectations() には、複数の呼び出しを記述できる
new Expectations() {{
someobj.dosomething(1, 2); result=3; times=1;
someobj.dosomething(3, 8); result=11; times=1;
someobj.dosomething(1, -2); result=-1; times=1;
}};
この順番で呼ばれないと Error が発生してテスト失敗
- 引数どうでも良い場合は
new Expectations() {{
someobj.dosomething(anyInt, anyInt); result=99;
}};
ほかの any*** は、→ http://jmockit.googlecode.com/svn/trunk/www/javadoc/mockit/Expectations.html
- 順番どうでも良い場合は
new NonStrictExpectations() {{
model1.getSum(); result = 99;
model1.add(anyInt);
}};
NonStrictExpectations?() は、あとで別途順番を検証できる
new Verifications() {{
model1.add(100);
model1.getSum();
}};
- 例外は result に入れる
new Expectations() {{
someobj.baka()
result=1; times=1;
result=2; times=1;
result = new BuddhaException("仏の顔も三度まで"); times=1;
}};
これは、baka() を三回呼ぶと例外が発生することをシミュレートする mock object
Java#Glassfish