在說另類思路之前,先說下傳統的測試方法:
0.準備一個干凈的測試數據庫環境
這個是前提
1.測試數據準備
使用文本,excel,或者wiki等,準備測試sql以及測試數據
利用dbfit,dbutil等工具將準備的測試數據導入到數據庫中
2.執行dao方法
執行被測試的dao方法
3.測試結果斷言
利用dbfit,dbutil等工具,斷言測試結果數據和預計是否一致
4.所有數據回滾
其實,對于這個流程來說,目前的dao測試框架,支持的已經比較完美了
但是此類測試方法,也有明顯的缺點(或者不能叫缺點,叫使用比較麻煩的地方)
如下:
1.背上了一個數據庫環境.
不輕量
這是一個共享環境,誰也無法確保環境數據是否真正的干凈
2.測試數據準備是一件麻煩的事情
新表,10幾個字段毫不為奇;老表,50幾個字段甚至百來個字段,也偶有可見;無論是使用文本,excel,wiki,準備工作量,都是巨大的.
準備的數據,部分字段內容可以是無意義的,部分字段內容又是需要符合測試意圖(testcase設計目的),部分字段還是其他表的關聯字段.從而導致后續維護人員無法了解準備數據意圖.
(實踐中,也出現過,一同事在維護他人單元測試時,由于無法了解測試數據準備意圖,寧可重新刪除,自己準備一份)
3.預計結果數據準備也是一件麻煩的事情
理由如上
所以,理論上是完美的測試方案,在實踐過程中,卻是一件麻煩的事情.導致DAO單元測試維護困難.
分析了現狀,我們再來分析下,IBatis下DAO,程序員主要做了哪些編碼:
1. 寫了一份sqlmap.xml配置文件
2. 通過
getSqlMapClientTemplate.doSomething($sqlID,$param), 執行語句
(當然,沒有使用spring的同學,也是使用了類似sqlMapClient.doSomething($sqlID,$param)方法)
而步驟2其實是框架替我們做了的事情,按照MOCK的思想,其實這部分代碼可以被MOCK的,那么我們是否可以做如下假設:
只要sqlmap.xml中配置信息(主要包括resultmap和statement)是正確的,那么執行結果也應該是正確的.
而我所謂的另類思路,就是基于這個假設,得出的:
IBatis下,DAO單元測試,我們拋棄背負的數據庫環境,只要根據不同的條件,斷言不同的sql即可.
于是乎,封裝了一個IbatisSqlTester,可以根據sqlmap中的statement和傳入的條件參數,生成sql語句.
那么,DAO單元測試就簡單了,脫離下數據庫環境:
public class ScoreDAOTest extends TestCase {
@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()中的方法.
}
}
優勢:
脫離了數據庫環境
脫離了表結構數據準備
脫離了預計結果數據準備
讓單元測試變成sql的斷言,編寫相對更簡單
缺點:
row mapper過程無法被測試
最后,附上兩個核心的代碼類(還未完成),供大家參考:
SqlStatement.java
/**
* <pre>
* SqlStatement:Sql語句對象.
* 包含:
* 1.sql語句,類似 select * from offer where id = ? and member_id = ?
* 2.參數值,類似 [1,stone2083]
*
* toString方法,返回執行的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參數
private Object[] param;
/**
* <pre>
* 輸出最終執行的sql內容.
* 將sql和param進行merge,產生最終執行的sql語句
* </pre>
*/
@Override
public String toString() {
return merge();
}
/**
* <pre>
* 將sql進行格式化.
*
* 目前只是簡單進行格式化.去除前后空格,已經重復空格
* TODO:請使用統一格式化標準規,建議使用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將根據提供的Sql Map Id 和 對應的參數,返回 {@link SqlStatement}對象,提供最終執行的sql語句
* 通過外部SqlAssert對象,將預計Sql和實際產生的Sql進行對比,判斷是否正確
* </pre>
*
* @author Stone.J 2010-8-9 下午02:58:46
*/
public class IbatisSqlTester {
// sqlMapClient
private ExtendedSqlMapClient sqlMapClient;
/**
* 根據提供的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;
}
/**
* 根據提供的SqlMap ID和對應的param信息,得到 {@link SqlStatement}對象
*
* @param sqlId: sql map id
* @param param: 參數內容
* @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