下面,我們開(kāi)始對(duì)DAO組件編寫單元測(cè)試。前面提到了HSQLDB這一小巧的純Java數(shù)據(jù)庫(kù)。HSQLDB除了提供完整的JDBC驅(qū)動(dòng)以及事務(wù)支持外,HSQLDB還提供了進(jìn)程外模式(與普通數(shù)據(jù)庫(kù)類似)和進(jìn)程內(nèi)模式(In-Process),以及文件和內(nèi)存兩種存儲(chǔ)模式。我們將HSQLDB設(shè)定為進(jìn)程內(nèi)模式及僅使用內(nèi)存存儲(chǔ),這樣,在運(yùn)行JUnit測(cè)試時(shí),可以直接在測(cè)試代碼中啟動(dòng)HSQLDB。測(cè)試完畢后,由于測(cè)試數(shù)據(jù)并沒(méi)有保存在文件上,因此,不必清理數(shù)據(jù)庫(kù)。
此外,為了執(zhí)行批量測(cè)試,在每個(gè)獨(dú)立的DAO單元測(cè)試運(yùn)行前,我們都執(zhí)行一個(gè)初始化腳本,重新建立所有的表。該初始化腳本是通過(guò)HibernateTool自動(dòng)生成的,稍后我們還會(huì)討論。下圖是單元測(cè)試的執(zhí)行順序:
對(duì)DAO編寫單元測(cè)試 圖-2
在編寫測(cè)試類之前,我們首先準(zhǔn)備了一個(gè)TransactionCallback抽象類,該類通過(guò)Template模式將DAO調(diào)用代碼通過(guò)事務(wù)包裝起來(lái):
public abstract class TransactionCallback {
public final Object execute() throws Exception {
Transaction tx = HibernateUtil.getCurrentSession().beginTransaction();
try {
Object r = doInTransaction();
tx.commit();
return r;
}
catch(Exception e) {
tx.rollback();
throw e;
}
}
// 模板方法:
protected abstract Object doInTransaction() throws Exception;
}
其原理是使用JDK提供的動(dòng)態(tài)代理。由于JDK的動(dòng)態(tài)代理只能對(duì)接口代理,因此,要求DAO組件必須實(shí)現(xiàn)接口。如果只有具體的實(shí)現(xiàn)類,則只能考慮CGLIB之類的第三方庫(kù),在此我們不作更多討論。
下面我們需要編寫DatabaseFixture,負(fù)責(zé)啟動(dòng)HSQLDB數(shù)據(jù)庫(kù),并在@Before方法中初始化數(shù)據(jù)庫(kù)表。該DatabaseFixture可以在所有的DAO組件的單元測(cè)試類中復(fù)用:
public class DatabaseFixture {
private static Server server = null; // 持有HSQLDB的實(shí)例
private static final String DATABASE_NAME = "javaeedev"; // 數(shù)據(jù)庫(kù)名稱
private static final String SCHEMA_FILE = "schema.sql"; // 數(shù)據(jù)庫(kù)初始化腳本
private static final List<String> initSqls = new ArrayList<String>();
@BeforeClass // 啟動(dòng)HSQLDB數(shù)據(jù)庫(kù)
public static void startDatabase() throws Exception {
if(server!=null)
return;
server = new Server();
server.setDatabaseName(0, DATABASE_NAME);
server.setDatabasePath(0, "mem:" + DATABASE_NAME);
server.setSilent(true);
server.start();
try {
Class.forName("org.hsqldb.jdbcDriver");
}
catch(ClassNotFoundException cnfe) {
throw new RuntimeException(cnfe);
}
LineNumberReader reader = null;
try {
reader = new LineNumberReader(new InputStreamReader(DatabaseFixture.class.getClassLoader().getResourceAsStream(SCHEMA_FILE)));
for(;;) {
String line = reader.readLine();
if(line==null) break;
// 將text類型的字段改為varchar(2000),因?yàn)镠SQLDB不支持text:
line = line.trim().replace(" text ", " varchar(2000) ").replace(" text,", " varchar(2000),");
if(!line.equals(""))
initSqls.add(line);
}
}
catch(IOException e) {
throw new RuntimeException(e);
}
finally {
if(reader!=null) {
try { reader.close(); } catch(IOException e) {}
}
}
}
@Before // 執(zhí)行初始化腳本
public void initTables() {
for(String sql : initSqls) {
executeSQL(sql);
}
}
static Connection getConnection() throws SQLException {
return DriverManager.getConnection("jdbc:hsqldb:mem:" + DATABASE_NAME, "sa", "");
}
static void close(Statement stmt) {
if(stmt!=null) {
try {
stmt.close();
}
catch(SQLException e) {}
}
}
static void close(Connection conn) {
if(conn!=null) {
try {
conn.close();
}
catch(SQLException e) {}
}
}
static void executeSQL(String sql) {
Connection conn = null;
Statement stmt = null;
try {
conn = getConnection();
boolean autoCommit = conn.getAutoCommit();
conn.setAutoCommit(true);
stmt = conn.createStatement();
stmt.execute(sql);
conn.setAutoCommit(autoCommit);
}
catch(SQLException e) {
log.warn("Execute failed: " + sql + "nException: " + e.getMessage());
}
finally {
close(stmt);
close(conn);
}
}
public static Object createProxy(final Object target) {
return Proxy.newProxyInstance(
target.getClass().getClassLoader(),
target.getClass().getInterfaces(),
new InvocationHandler() {
public Object invoke(Object proxy, final Method method, final Object[] args) throws Throwable {
return new TransactionCallback() {
@Override
protected Object doInTransaction() throws Exception {
return method.invoke(target, args);
}
}.execute();
}
}
);
}
}