IBatis下DAO單元測試另類思路
在說另類思路之前,先說下傳統(tǒng)的測試方法:0.準備一個干凈的測試數(shù)據(jù)庫環(huán)境
這個是前提
1.測試數(shù)據(jù)準備
使用文本,excel,或者wiki等,準備測試sql以及測試數(shù)據(jù)
利用dbfit,dbutil等工具將準備的測試數(shù)據(jù)導入到數(shù)據(jù)庫中
2.執(zhí)行dao方法
執(zhí)行被測試的dao方法
3.測試結果斷言
利用dbfit,dbutil等工具,斷言測試結果數(shù)據(jù)和預計是否一致
4.所有數(shù)據(jù)回滾
其實,對于這個流程來說,目前的dao測試框架,支持的已經(jīng)比較完美了
但是此類測試方法,也有明顯的缺點(或者不能叫缺點,叫使用比較麻煩的地方)
如下:
1.背上了一個數(shù)據(jù)庫環(huán)境.
不輕量
這是一個共享環(huán)境,誰也無法確保環(huán)境數(shù)據(jù)是否真正的干凈
2.測試數(shù)據(jù)準備是一件麻煩的事情
新表,10幾個字段毫不為奇;老表,50幾個字段甚至百來個字段,也偶有可見;無論是使用文本,excel,wiki,準備工作量,都是巨大的.
準備的數(shù)據(jù),部分字段內容可以是無意義的,部分字段內容又是需要符合測試意圖(testcase設計目的),部分字段還是其他表的關聯(lián)字段.從而導致后續(xù)維護人員無法了解準備數(shù)據(jù)意圖.
(實踐中,也出現(xiàn)過,一同事在維護他人單元測試時,由于無法了解測試數(shù)據(jù)準備意圖,寧可重新刪除,自己準備一份)
3.預計結果數(shù)據(jù)準備也是一件麻煩的事情
理由如上
所以,理論上是完美的測試方案,在實踐過程中,卻是一件麻煩的事情.導致DAO單元測試維護困難.
分析了現(xiàn)狀,我們再來分析下,IBatis下DAO,程序員主要做了哪些編碼:
1. 寫了一份sqlmap.xml配置文件
2. 通過getSqlMapClientTemplate.doSomething($sqlID,$param), 執(zhí)行語句
(當然,沒有使用spring的同學,也是使用了類似sqlMapClient.doSomething($sqlID,$param)方法)
而步驟2其實是框架替我們做了的事情,按照MOCK的思想,其實這部分代碼可以被MOCK的,那么我們是否可以做如下假設:
只要sqlmap.xml中配置信息(主要包括resultmap和statement)是正確的,那么執(zhí)行結果也應該是正確的.
而我所謂的另類思路,就是基于這個假設,得出的:
IBatis下,DAO單元測試,我們拋棄背負的數(shù)據(jù)庫環(huán)境,只要根據(jù)不同的條件,斷言不同的sql即可.
于是乎,封裝了一個IbatisSqlTester,可以根據(jù)sqlmap中的statement和傳入的條件參數(shù),生成sql語句.
那么,DAO單元測試就簡單了,脫離下數(shù)據(jù)庫環(huán)境:
@SpringBeanByName
private IbatisSqlTester ibatisSqlTester; //通過spring配置,需要注入sqlmapclient對象
@Test
public void testListTpScores() {
Map<String, Object> param = new HashMap<String, Object>(1);
param.put("memberIds", new String[] { "stone", "stone2083" });
SqlStatement sql = ibatisSqlTester.test("MS-LIST-SCORES", param);
// sql全部匹配
SqlAssert.isEqual("select * from score where member_id in ('stone','stone2083')", sql.toString());
// sql包含member_id,athena2002,stone關鍵詞
SqlAssert.keyWith(sql.toString(), "member_id", "stone", "stone2083");
// sql符合某個 正則
SqlAssert.regexWith(".* where member_id in .*", sql.toString());
//其中,SqlAssert也可以換 成want.string()中的方法.
}
}
優(yōu)勢:
脫離了數(shù)據(jù)庫環(huán)境
脫離了表結構數(shù)據(jù)準備
脫離了預計結果數(shù)據(jù)準備
讓單元測試變成sql的斷言,編寫相對更簡單
缺點:
row mapper過程無法被測試
最后,附上兩個核心的代碼類(還未完成),供大家參考:
SqlStatement.java
* <pre>
* SqlStatement:Sql語句對象.
* 包含:
* 1.sql語句,類似 select * from offer where id = ? and member_id = ?
* 2.參數(shù)值,類似 [1,stone2083]
*
* toString方法,返回執(zhí)行的sql語句,如:
* select * from offer where id = '1' and member_id = 'stone2083'
* </pre>
*
* @author Stone.J 2010-8-9 下午02:55:36
*/
public class SqlStatement {
//sql
private String sql;
//sql參數(shù)
private Object[] param;
/**
* <pre>
* 輸出最終執(zhí)行的sql內容.
* 將sql和param進行merge,產(chǎn)生最終執(zhí)行的sql語句
* </pre>
*/
@Override
public String toString() {
return merge();
}
/**
* <pre>
* 將sql進行格式化.
*
* 目前只是簡單進行格式化.去除前后空格,已經(jīng)重復空格
* TODO:請使用統(tǒng)一格式化標準規(guī),建議使用SqlFormater類,進行處理
* </pre>
*
* @param sql
* @return
*/
protected String format(String sql) {
if (sql == null) {
return null;
}
return sql.toLowerCase().trim().replaceAll("\\s{1,}", " ");
}
/**
* <pre>
* 將sql和param進行merge.
* TODO:請嚴格按照SQL標準,進行merge sql內容
* </pre>
*/
protected String merge() {
if (param == null || param.length == 0) {
return this.sql;
}
String ret = sql;
for (Object p : param) {
ret = ret.replaceFirst("\\?", "'" + p.toString() + "'");
}
return ret;
}
public String getSql() {
return sql;
}
public void setSql(String sql) {
this.sql = format(sql);
}
public Object[] getParam() {
return param;
}
public void setParam(Object[] param) {
this.param = param;
}
}
IbatisSqlTester.java
* <pre>
* IBtatis SQL 測試
* 一般IBatis DAO單元測試,主要就是在測試ibatis的配置文件.
* IbatisSqlTester將根據(jù)提供的Sql Map Id 和 對應的參數(shù),返回 {@link SqlStatement}對象,提供最終執(zhí)行的sql語句
* 通過外部SqlAssert對象,將預計Sql和實際產(chǎn)生的Sql進行對比,判斷是否正確
* </pre>
*
* @author Stone.J 2010-8-9 下午02:58:46
*/
public class IbatisSqlTester {
// sqlMapClient
private ExtendedSqlMapClient sqlMapClient;
/**
* 根據(jù)提供的SqlMap ID,得到 {@link SqlStatement}對象
*
* @param sqlId: sql map id
* @return @see {@link SqlStatement}
*/
public SqlStatement test(String sqlId) {
//得到MappedStatement對象
MappedStatement ms = sqlMapClient.getMappedStatement(sqlId);
if (ms == null) {
//TODO:建議封轉自己的異常對象
throw new RuntimeException("can't find MappedStatement.");
}
//按照Ibatis代碼,得到Sql和Param信息
RequestScope request = new RequestScope();
ms.initRequest(request);
Sql sql = ms.getSql();
String sqlValue = sql.getSql(request, null);
//組轉返回對象
SqlStatement ret = new SqlStatement();
ret.setSql(sqlValue);
return ret;
}
/**
* 根據(jù)提供的SqlMap ID和對應的param信息,得到 {@link SqlStatement}對象
*
* @param sqlId: sql map id
* @param param: 參數(shù)內容
* @return @see {@link SqlStatement}
*/
public SqlStatement test(String sqlId, Object param) {
//得到MappedStatement對象
MappedStatement ms = sqlMapClient.getMappedStatement(sqlId);
if (ms == null) {
//TODO:建議封轉自己的異常對象
throw new RuntimeException("can't find MappedStatement.");
}
//按照Ibatis代碼,得到Sql和Param信息
RequestScope request = new RequestScope();
ms.initRequest(request);
Sql sql = ms.getSql();
String sqlValue = sql.getSql(request, param);
Object[] sqlParam = sql.getParameterMap(request, param).getParameterObjectValues(request, param);
//組轉返回對象
SqlStatement ret = new SqlStatement();
ret.setSql(sqlValue);
ret.setParam(sqlParam);
return ret;
}
/**
* 設置SqlMapClient對象
*/
public void setSqlMapClient(ExtendedSqlMapClient sqlMapClient) {
this.sqlMapClient = sqlMapClient;
}
/**
* <pre>
* 不推薦使用
* 推薦使用: {@link IbatisSqlTester#setSqlMapClient(ExtendedSqlMapClient)}
* TODO:請去除這個方法,或者增加初始化的方式
* </pre>
*
* @param sqlMapConfig sqlMapConfig xml文件
*/
public void setSqlMapConfig(String sqlMapConfig) {
InputStream in = null;
try {
File file = ResourceUtils.getFile(sqlMapConfig);
in = new FileInputStream(file);
this.sqlMapClient = (ExtendedSqlMapClient) SqlMapClientBuilder.buildSqlMapClient(in);
} catch (Exception e) {
throw new RuntimeException("sqlMapConfig init error.", e);
} finally {
if (in != null) {
try {
in.close();
} catch (IOException e) {
}
}
}
}
}
最后的最后附上所有代碼(通過單元測試代碼,可以看如何使用).歡迎大家的討論.
sqltester
builder
posted on 2010-08-12 09:03 stone2083 閱讀(3534) 評論(9) 編輯 收藏 所屬分類: java