网址: http://www.chiefsailor.net/blog/2010/11/junit-unit-testing-use-stub-agile/
一、单元测试
单 元测试的一个目的就是能够对类的实现进行快速的反馈。这里面有两点很重要,一个是快速,也就是很短的时间,至少要比我们部署功能、点开页面、验证要快,一 般我们限制每个单元测试的执行时间在1秒以内;另外一个是结果反馈,就是能够告诉我这个类的方法实现的正确与否,当我改变代码的时候对类原有的功能有没有 影响。
从上面来看,单元测试是一个很好的开发助手,如果一个类的内聚性很高,不依赖于其他类的时候,相对测试比较容易,但是在实际开发中, 我们经常会遇到依赖其他外部接口的情况,比如:调用其他系统Web Service,dll库;或者是调用本系统内的其他模块的接口;或者是访问数据库,网页等其他资源。这些情况下使用单元测试就会相对麻烦一些,而且会让 单元测试的独立性不强,依赖于具体的实现。
本文中我们将以一个账户接口类为例,介绍使用Stub技术进行单元测试,以实现快速反馈的目的。
二、单元测试前的账户接口
需要实现的账户接口类中,包括存款和查询余额两个方法。由于用户的存款信息要持久化到数据库中,所以我们调用了AccountDao这个类来实现基本的操作。
本文中的账户接口类是AccountService,它和AccountDao类的最开始的实现代码分别如清单1和清单2所示。
清单1:AccountService类代码
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
|
public
class
AccountService {
public
void
deposit(String customerId,
long
money) {
Connection conn =
null
;
AccountDao accountDao =
new
AccountDao ();
try
{
conn = Util.getConnection();
conn.setAutoCommit(
false
);
accountDao.addMoney(conn, customerId, money);
conn.setAutoCommit(
true
);
}
catch
(Exception ex) {
conn.roolback();
}
finally
{
if
(!conn.isCloseed()){
conn.close();
}
}
}
public
long
queryBalance(String customerId) {
Connection conn =
null
;
long
money;
AccountDao accountDao =
new
AccountDao ();
try
{
conn = Util.getConnection();
money = accountDao.getMoney(conn, customerId);
}
catch
(Exception ex) {
logger.logError(ex);
}
finally
{
if
(!conn.isCloseed()){
conn.close();
}
}
return
money;
}
}
|
清单2:AccountDao类代码
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
public
class
AccountDao {
public
void
addMoney(Connection conn, String customerId,
long
money)
throws
SqlException {
String sql = getAddMoneySql();
PreparedStatement pstms = conn.preparedStatement(sql);
pstms.setString(
1
, customerId);
pstms.setLong(
2
, money);
pstms.executeUpdate();
}
public
long
getMoney(Connection conn, String customerId)
throws
SqlException {
long
money;
String sql = getQueryMoneySql();
PreparedStatement pstms = conn.preparedStatement(sql);
pstms.setString(
1
, customerId);
ResultSet rs = pstms.executeQuery();
while
(rs.next()){
money = rs.getInt(
1
);
}
return
money;
}
}
|
三、4步改造,实现对AccountService的单元测试
在 清单1中的AccountService类与AccountDao类是紧密耦合在一起的,为了能够测试接口,我们必须要进行整体的部署或者实际的读、存数 据库。这样和我们的单元测试的快速性是完全违背的(数据库集成测试不在本文讨论范围)。那如何屏蔽掉数据库对单元测试的影响呢?
第1步: 我们要抽取一个AccountDao 的上级接口类,我们暂定为IAccoutDao,这个接口包括Dao的两个方法。并让AccountDao实现 IAccoutDao接口。
清单3:IAccoutDao接口类代码
1
2
3
4
|
public
class
IAccountDao {
public
void
addMoney(Connection conn, String customerId,
long
money)
throws
SqlException ;
public
long
getMoney(Connection conn, String customerId)
throws
SqlException ;
}
|
面向接口编程是一个很好的设计思想,它能够帮助我们轻松的用实现类来替换原有的接口。
第2步: 我们将AccountService方法中不断调用的Connection和AccountDao抽取成类变量,并增加对应的getter和Setter方法。注意这里的AccountDao变量我们定义成IAccoutDao类型。
清单4:改造后的AccountService类
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
|
public
class
AccountService {
Connection conn;
IAccoutDao accountDao;
public
void
deposit(String customerId,
long
money) {
try
{
conn.setAutoCommit(
false
);
accountDao.addMoney(conn, customerId, money);
conn.setAutoCommit(
true
);
}
catch
(Exception ex) {
conn.roolback();
}
finally
{
if
(!conn.isCloseed()){
conn.close();
}
}
}
public
long
queryBalance(String customerId) {
long
money;
try
{
money = accountDao.getMoney(conn, customerId);
}
catch
(Exception ex) {
logger.logError(ex);
}
finally
{
if
(!conn.isCloseed()){
conn.close();
}
}
return
money;
}
public
Connection getConn() {
return
conn;
}
public
void
setConn(Connection conn) {
this
.conn= conn;
}
public
IAccoutDao getAccoutDao () {
return
accoutDao ;
}
public
void
setAccoutDao(IAccoutDao accoutDao ) {
this
.accoutDao = accoutDao;
}
}
|
这种方法利用的是依赖注入的思想,而setter方法作为依赖注入的一种重要实现形式,在单元测试中有着广泛的应用。
第3步: 使用stub技术构建一个StubAccoutDao,它实现了IAccoutDao 接口。先不解释stub技术的含义,大家先看清单5中它的实现。
清单5:StubAccoutDao的实现类
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
|
public
class
StubAccoutDao
implements
IAccoutDao{
public
Map<String, Long> accountsMap =
new
HashMap<String, Long>();
public
void
addMoney(Connection conn, String customerId,
long
money)
throws
SqlException {
if
(accounts.get(customerId) ==
null
) {
accounts.put(customerId, money);
}
else
{
long
before = accounts.get(customerId);
accounts.put(customerId, before + money);
}
}
public
long
getMoney(Connection conn, String customerId)
throws
SqlException {
return
accounts.get(customerId);
}
}
|
看完这个类,大家一定就明白了,stub就是一个模拟的实现,但是能够反馈正确的结果。这也就是stub技术的主体思想。
第4步: 进行单元测试。如清单6中所示,我们基于上面的StubAccoutDao实现,对AccountService类进行单元测试。对于它的两个方法,我们能够轻松的排除了对数据库的依赖,验证逻辑是否正确。这里面的 SetUp方法在每个单元测试前都执行一次,帮助每个单元测试建立独立的测试环境。
清单6:单元测试类AccountServiceTest
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
|
public
class
AccountServiceTest {
IAccoutDao accountDao ;
Connection conn ;
AccountService accountService;
@Before
public
void
setUp() {
accountDao =
new
StubAccoutDao();
conn =
new
Connection();
accountService =
new
AccountService();
accountService.setConn(conn);
accountService.setAccountDao(accountDao);
}
@Test
public
void
testDeposit() {
accountService.deposit(
"chiefsailor"
,
100
);
long
money = accountDao.getMoney(
"chiefsailor"
);
assertEquals(
100
, money);
}
@Test
public
long
testQueryBalance() {
long
expected =
100
;
accountDao.accountsMap.put(
"chiefsailor"
, expected);
long
actual = accountService.queryBalance();
assertEquals(expected, actual);
}
}
|
注:本文把目标集中在对AccoutDao 的stub技术上,对conn变量也可以用同样的方法进行实现。
四、总结
总体来讲,stub技术就是模拟了一个依赖的外部接口的简单实现,从而帮助我们解除依赖进行单元测试。在使用过程中,stub技术往往是与面向接口编程、依赖注入结合起来使用的,stub的实现框架常用的还有对java服务器模拟的Jetty。
stub技术也有一些不足的地方,比如对很复杂的类的实现起来也会比较麻烦,同时对细粒度的单元测试的支持性能也不够好。但是对于帮助我们进行快速的开发,比如在与复杂接口进行交互,或是其他系统的功能尚未完成时特别有效。