DAO 層單元測試面臨三大難題:
採用 HSQLDB 內存模式 作為測試期數據庫:
將事務邊界(開啟/提交/回滾)封裝為模板方法,業務代碼僅實現 doInTransaction():
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;
}
集成 JUnit 4 生命周期,實現「測試類級啟動 DB、方法級重建表」:
public class DatabaseFixture {
private static Server server; // HSQLDB 服務器實例
private static final String DB_NAME = "javaeedev";
private static final String SCHEMA_FILE = "schema.sql";
private static final List<String> initSqls = new ArrayList<>();
@BeforeClass
public static void startDatabase() throws Exception {
// 單例啟動內存數據庫
if (server != null) return;
server = new Server();
server.setDatabaseName(0, DB_NAME);
server.setDatabasePath(0, "mem:" + DB_NAME); // 關鍵:內存模式
server.setSilent(true);
server.start();
// 載入並預處理 Schema 腳本(兼容 HSQLDB 類型差異)
loadSchema();
}
@Before
public void initTables() {
// 每個 @Test 前重建表,保證乾淨環境
for (String sql : initSqls) executeSQL(sql);
}
// 動態代理:為 DAO 自動織入事務邊界
public static Object createProxy(Object target) {
return Proxy.newProxyInstance(
target.getClass().getClassLoader(),
target.getClass().getInterfaces(),
(proxy, method, args) -> new TransactionCallback() {
@Override protected Object doInTransaction() throws Exception {
return method.invoke(target, args);
}
}.execute()
);
}
// ... 省略 executeSQL、getConnection 等工具方法
}
public class UserDaoImplTest extends DatabaseFixture {
private final UserDao userDao = new UserDaoImpl();
private final UserDao proxy = (UserDao) createProxy(userDao); // 事務代理
@Test
public void testQueryUser() {
User user = newUser("test");
proxy.createUser(user); // 自動事務
User found = proxy.queryUser("test");
assertEquals(user.getEmail(), found.getEmail());
}
@Test(expected = ResourceNotFoundException.class)
public void testQueryNonExistUser() throws Exception {
// 異常測試需繞過代理,直接用原始 DAO + 手動事務模板
new TransactionCallback() {
@Override protected Object doInTransaction() throws Exception {
userDao.queryUser("nonexist");
return null;
}
}.execute();
}
}
關鍵點:正常測試用 proxy 享受自動事務;異常測試用 userDao 配合 TransactionCallback 避免異常被 UndeclaredThrowableException 包裹。
避免手寫 SQL,從實體註解自動導出 schema.sql:
<target name="make-schema" depends="build">
<taskdef name="hibernatetool" classname="org.hibernate.tool.ant.HibernateToolTask">
<classpath refid="build-classpath"/>
</taskdef>
<hibernatetool destdir="${gen.dir}">
<classpath refid="build-classpath"/>
<annotationconfiguration configurationfile="${src.dir}/hibernate.cfg.xml"/>
<hbm2ddl export="false" drop="true" create="true" delimiter=";"
outputfilename="schema.sql" destdir="${src.dir}"/>
</hibernatetool>
</target>
所有問題在編譯期/測試期暴露,而非運行期由用戶發現。
「HSQLDB 內存庫 + 事務模板 + 自動 Schema + JUnit 4」構建了輕量、隔離、可重複的 DAO 測試體系。此方案不侵入業務代碼,配置一次復用處處,是 J2EE 專案落地單元測試的高性價比選擇。