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