做
java
企业级开发时,我们通常采用三层架构。特别地,如果我们要做的系统的业务逻辑不是很复杂时,我们要处理的不过是
CRUD
操作,这时我们可能将
dao
层与
service
层合并为一层,尽管很多人会这样做,但我仍倾向于将两层分开;因为
service
与
dao
不是一一对应的,从复用及逻辑清晰的角度考虑,应该将它们分开。在三层架构下,对于
web
层,
service
层,
dao
层我们都该怎么测试?这里我将介绍基于
Spring
,
Hibernate
和
DbUnit
的情况下我的测试方法。由于使用了
Spring
,事务管理就不在
dao
,因此要单独地测试
dao
可能要麻烦一些;另一方面,
dao
中的操作大多是简单的,也不是很值得测试。在使用了
Hibernate
和
Spring
的情况下,我们要测试的除了
HQL
,还有其配置文件,我觉得对数据持久化的测试最好定在
service
上。如果
service
业务逻辑复杂的话,与数据持久化无关的业务逻辑(应该写在领域对象中)可以单独测试
,在保证与数据持久化无关的业务逻辑的正确性下,带上
dao
操作做集成(单元)测试。
在使用
Spring
做测试时,推荐测试类继承自
Spring
的
AbstractTransactionalDataSourceSpringContextTests
,这也是官方推荐的做法。
AbstractTransactionalDataSourceSpringContextTests
对测试方法提供了事务管理,同时它的依赖注入特性也方便测试类注入被测试类(但我并不认为它的事务管理是很需要的)。承接于上一篇文章中的例子
,下面给出测试类
AccountServiceTest。
package
hibernatesample.service;
import
hibernatesample.domain.Account;
import
java.io.File;
import
java.io.InputStream;
import
java.util.List;
import
org.dbunit.IDatabaseTester;
import
org.dbunit.JdbcDatabaseTester;
import
org.dbunit.dataset.IDataSet;
import
org.dbunit.dataset.xml.FlatXmlDataSet;
import
org.springframework.test.AbstractDependencyInjectionSpringContextTests;
import
org.springframework.test.AbstractTransactionalDataSourceSpringContextTests;
public
class
AccountServiceTest
extends
AbstractTransactionalDataSourceSpringContextTests {
private
AccountService accountService;
private
IDatabaseTester databaseTester;
public
AccountService getAccountService() {
return
accountService;
}
public
AccountServiceTest()
throws
Exception{
}
public
void
setAccountService(AccountService accountService) {
this
.accountService
=
accountService;
}
@Override
protected
void
onSetUp()
throws
Exception {
databaseTester
=
new
JdbcDatabaseTester(
"
com.mysql.jdbc.Driver
"
,
"
jdbc:mysql://localhost/HibernateSample
"
,
"
root
"
,
"
0102
"
);
IDataSet dataSet
=
getDataSet();
databaseTester.setDataSet( dataSet );
databaseTester.onSetup();
}
@Override
protected
void
onTearDown()
throws
Exception {
databaseTester.onTearDown();
}
protected
IDataSet getDataSet()
throws
Exception {
String path
=
"
hibernatesample
"
+
File.separator
+
"
dao
"
+
File.separator
+
"
dataset
"
+
File.separator
+
"
Account.xml
"
;
InputStream in
=
this
.getClass().getClassLoader().getResourceAsStream(path);
return
new
FlatXmlDataSet(in);
}
@Override
protected
String[] getConfigLocations() {
return
new
String[]{
"
classpath:service-applicationContext.xml
"
};
}
public
void
testInsert() {
Account a
=
new
Account();
a.setName(
"
aa
"
);
accountService.insertAccount(a);
List
<
Account
>
l
=
accountService.findAllAccount();
assertEquals(
3
, l.size());
Account b
=
l.get(
2
);
assertEquals(
"
aa
"
, b.getName());
}
public
void
testFindAll() {
List
<
Account
>
l
=
accountService.findAllAccount();
assertEquals(
2
, l.size());
Account a
=
l.get(
0
);
assertEquals(
new
Long(
1
), a.getId());
assertEquals(
"
kafka
"
, a.getName());
Account b
=
l.get(
1
);
assertEquals(
new
Long(
2
), b.getId());
assertEquals(
"
0102
"
, b.getName());
}
}
由于
AccountServiceTest
继承了
AbstractTransactionalDataSourceSpringContextTests
,因此使用
D
b
Unit
时就不能继承
DBTestCase
,这里采用的方法就是在
onSetUp()
中实现数据集的装入。由于
AbstractTransactionalDataSourceSpringContextTests
最第
N
个顶层的父类继承于
Junit
,所以
onSetUp()
将在
SetUp()
中被调用,默认的
onSetUp()
实现为空;
onTearDown()
的原理和
onSetUp()
相似。由于
AbstractTransactionalDataSourceSpringContextTests
具有依赖注入的特性,所以被测试对象
accountService
可以通过
setter
方法获得;当然,我们也可以在构造函数中通过
Bean
工厂获得
accountService
。
protected
String[] getConfigLocations()
返回的数组为
Spring
Beans
配置文件的位置。
对于与测试相关的其他类,这里就不给出了。通过使用
DbUnit
,我觉得测试持久化操作时方便了许多。不爽的地方在于,在使用了
Spring
,
Hibernate
的情况下,测试一个方法有些耗时,这自然不单单是
DbUnit
的问题。但如果我将
dao
和
service
的测试放在一起来做,这种耗时也只能忍着。再说一下
web
层的测试,我当前使用的
web
框架是
Webwork
,在测试
Action
的时候,对于
Web
请求之类使用
Mock
,对于
Service
的调用有时
Mock
有时真实调用。
Mock
的好处是耗时能少一些,但写
Mock
代码也很繁琐,而对
service
的
Mock
有时本身就失掉了测试的意义。