在说另类思路之前,先说下传统的测试方法:
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