注:本文基本翻译自DbUnit的文档,翻译不好朋友见谅
注:本文基本翻译自DbUnit的文档,翻译不好朋友见谅
DBUnit是JUnit的一个扩展,对于数据库驱动的项目而言(基本上所有的Web项目都是数据库驱动的),对于服务层的单元测试非常麻烦,因为不能保证每次测试时数据库都是同一个状态,所以开发者不敢写断言(assertEquals())。我个人也是因为这个原因所以对驱动测试开发总是敬而远之。
有了DBUnit,一切都变了,DBUnit的目的就是在每个单元测试运行之前将数据库初始化成一个预定义的状态,以保证单元测试时的断言不会因为数据库状态发生了变化而失败,同时可以解决前一个单元测试失败导致对数据库的操作未按照测试用例执行而影响后一个单元测试的问题。
DBUnit可以将数据库中的数据导出成XML数据集,也可以将XML数据集重新导入到数据库中,这也是DBUnit实现测试前初始化数据库状态的实现方法,而且,DBUnit还可以验证数据库中的模式和数据是不是和XML数据集中的模式和数据是否一样,也就是真正意义上的“数据库测试”,不过DBUnit貌似还不支持数据库存储过程、触发器、视图等高级功能的测试。DBUnit2.0增强了对于流模式下巨型XML数据集的支持。
相信信仰单元测试驱动开发或者有意向在开发中加入单元测试的开发者很容易理解上面的叙述,下面用一个示例演示DBUnit的功能。
假设我们开发中已经建立了一个数据库,里面只有一个数据表user:
create table user
(
id bigint auto_increament primary key,
username char(48) not null,
password char(48) not null,
name varchar(48) not null
);
其中有一行数据:
insert into user(id, username, password, name) values(1, 'admin', '123', 'Administrator');
第一步是根据测试数据建立XML数据集,DBUnit用下面的格式来表示一行数据:
<user id="1" username="admin" password="123" name="Administrator" />
一个XML数据集应该具有如下格式:
<?xml version="1.0" encoding="UTF-8"?> <dataset> <user id="1" username="admin1" password="123" name="Administrator1" /> <user id="2" username="admin2" password="123" name="Administrator2" /> <user id="3" username="admin3" password="123" name="Administrator3" /> </dataset>
一般,最好对每个需要数据库驱动的测试用例都建立一个XML数据集,一方面可以保证单元测试之间完全的数据独立,另一方面可以只设置测试需要的数据,尽量使XML数据集保持简单可控。
第二步就可以开始编码了,DBUnit提供了一个抽象类DatabaseTestCase,这个类继承了JUnit的TestCase类,其中定义了两个抽象方法:
/**
* 返回测试用的数据库连接对象
*/
protected abstract IDatabaseConnection getConnection() throws Exception;
/**
* 返回测试用XML数据集对象
*/
protected abstract IDataSet getDataSet() throws Exception;
getConnection()的返回值类型为IDatabaseConnection,对java.sql.Connection进行了简单的封装,可以简单的实现如下:
protected IDatabaseConnection getConnection() throws Exception {
Class.forName("com.mysql.jdbc.Driver");
Connection conn = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/dbunitdemo", "root", "admin");
return new MySqlConnection(conn, "dbunitdemo");//本人用的是MySQL
}
getDataSet()的返回值是IDataSet类型,即从XML数据集中读取数据和结构,实例化为IDataSet对象,可以简单实现如下:
protected IDataSet getDataSet() throws Exception {
InputStream is = new FileInputStream("dbunitdemo-seed.xml");
return new FlatXmlDataSet(is);
}
定义了以上两个方法后,DBUnit就可以工作了,但是不少人会就会想到一个问题,如果数据库中存在以前的数据,而我需要将这些数据删除,DBUnit会自动的帮我做这些事情么?事实上,很显然DBUnit会,其默认操作就是删除数据,同时将XML数据集中的数据插入到数据库中,不过DBUnit也允许我们通过覆盖DatabaseTestCase中的getSetUpOperation()方法来实现自己的动作,在DatabaseTestCase中默认实现是这样的:
protected DatabaseOperation getSetUpOperation() throws Exception
{
return DatabaseOperation.CLEAN_INSERT;
}
DatabaseOperation是DBUnit定义的数据库操作对象,CLEAN_INSERT代表清空数据、插入XML数据集数据的操作。
与SetUpOperation对象就有TearDownOperation,表示结束测试的时候做的事情,DatabaseTestCase的默认实现是什么都不做:
protected DatabaseOperation getTearDownOperation() throws Exception同样,开发者可以根据自己的需求来改变默认操作。
{
return DatabaseOperation.NONE;
}
基本原理就已经讲解完了,我写了一个例子程序,实现了一个领域模型类(User),一个Dao接口(IUserDao),定义了list\save\get\delete四个基本方法,一个用JDBC实现的Dao接口实现类(UserDaoImpl),建立一个UserDaoTest对JBDC版本的Dao实现类进行测试。
例子程序下载地址:dbunitdemo_jdbc_1.0.zip
另外这个工程使用maven进行build,如果你对maven还是不很了解,请查考《Maven权威指南》
下载工程后,在项目根目录下执行mvn sql:execute,将使用Maven Sql插件导入src/main/sql/dbunitdemo.sql中的数据库脚本到数据库中。
然后可以直接运行mvn test进行单元测试,测试报告会在命令行输出,同时在target\surefire-reports下也会生成DBUnit报告
暂时不支持万以上级别,后面会加上这个。道理很简单,但我搜了一下google,找到的代码没有几个能直接用,所以分享下,直接贴代码了:
public static int chnNumToIntConverter(String chnNum){
HashMap<String, Integer> numbers = new HashMap<String, Integer>();
numbers.put(”零”, 0);
numbers.put(”一”, 1);
numbers.put(”二”, 2);
numbers.put(”三”, 3);
numbers.put(”四”, 4);
numbers.put(”五”, 5);
numbers.put(”六”, 6);
numbers.put(”七”, 7);
numbers.put(”八”, 8);
numbers.put(”九”, 9);
HashMap<String, Integer> units = new HashMap<String, Integer>();
units.put(”十”, 10);
units.put(”百”, 100);
units.put(”千”, 1000);
int result = 0;
//补全“十三”这种类型前的“一”
if(chnNum.startsWith(”十”)){
chnNum = “一” + chnNum;
}
int length = chnNum.length();
for(int i=0; i<length; i++){
String num = chnNum.substring(i, i+1);
if(! num.equals(”零”)){
if(i+2 > length){
result += numbers.get(num);
}else{
String unit = chnNum.substring(i+1, i+2);
result += numbers.get(num) * units.get(unit);
}
i++;
}
}
return result;
}
开发中对一个working copy进行update操作的时候出现了标题所示的错误,因为在update之前对其进行了switch操作,因此猜想可能是因为这个缘故,上网查了一下,找到一篇文章《SVN merger errors: Working copy path ‘*’ does not exist in repo》,按照其描述的方法并不能解决我的问题,但是让我对.svn文件夹下的这个entry文件产生了兴趣,仔细的看了一下,发现这个文件里面有一行是写的版本号,紧接着下一行就是文件夹路径,但是这个文件夹路径是错误的。
对其进行了改正后,重新update,正常了,看来是svn的一个bug吧。
为什么会产生这个异常:
使用Lucene检索过程中如果用到RangeQuery,PrefixQuery,WildcardQuery,FuzzyQuery这四种Query,可能会产生TooManyClauses异常。为什么会产生这个异常呢?举例说明:
以RangeQuery为例,如果日期范围为19990101到20091231,在索引文件中有19990102,19990103等等这些日期词组,那么RangeQuery会被扩展成“19990102 OR 19990103”,成了2个子句。可以想象,如果索引文件里面在这个时间段内的日期有很多,那么就会产生很多子句。
PrefixQuery等也是同样的道理,如查询词为“法*”,如果索引文件中有“法律”、“法场”、“法医”、“法典”等等,这个Query就会被扩展成“法律 OR 法场 OR 法医 OR 法典”,或许会更多更多。
而为了节省内存,Lucene默认将子句数目限制为1024,如果超出限制,就会抛出TooManyClauses异常。
怎么解决这个问题呢,Lucene提供了三种方法:
【1】使用filter替代Query,当然这是以牺牲查询速度为代价的,不过可以通过缓存的方式缓解这个问题。仍然以前面RangeQuery为例,可以使用RangeFilter来替代RangeQuery,如下:
之前的代码:
1 2 3 4 5 | BooleanQuery simpleQuery = new BooleanQuery(); Term dateLower = new Term("publishDate", startYear + "0101"); Term dateUpper = new Term("publishDate", endYear + "1231"); RangeQuery dateQuery = new RangeQuery(dateLower, dateUpper, true); simpleQuery.add(dateQuery, Occur.MUST); |
之后的代码:
1 2 3 | BooleanQuery simpleQuery = new BooleanQuery(); RangeFilter dateFilter = new RangeFilter("publishDate", startYear + "0101", endYear + "1231", true, true); FilteredQuery filteredQuery = new FilteredQuery(simpleQuery, dateFilter); |
【2】通过BooleanQuery.setMaxClauseCount(10240)来限制数目。这样会加大内存的消耗。使用BooleanQuery.setMaxClauseCount(Integer.MAX_VALUE),可以完全去掉这个限制。
【3】对于范围查询,可以尽可能的降低精度,比如如果查询不需要精确到月份与日期,只需要精确到年,据说可以使用DateTools这个类可以很简单解决时间转化问题,本人并没有尝试。