单元测试初探 — 使用Stub快速反馈

作者: chiefsailor | 版权声明: 可以转载, 但必须以超链接形式标明文章原始出处和作者信息及版权声明
网址: 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技术也有一些不足的地方,比如对很复杂的类的实现起来也会比较麻烦,同时对细粒度的单元测试的支持性能也不够好。但是对于帮助我们进行快速的开发,比如在与复杂接口进行交互,或是其他系统的功能尚未完成时特别有效。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值