单元测试 实践

     测试的要求:
       测试对一个Account的Dao操作以及Service。
java 代码
 
  1. //先来测试最基本的dao吧  
  2. package org.wuhua.dao;  
  3.   
  4. import java.util.Collection;  
  5.   
  6. public interface IBaseDao {  
  7.     Object save(Object o);  
  8.     void delete(Object o);  
  9.     Object update(Object o);  
  10.     Collection list();  
  11. }  
根据我的理解,测试的对方要跟mock的对象分开,(开始我一直认为你要mock的对象就是你要测试的东西,搞着搞着,我就很迷茫了。)。现在要做的就是看你IBaseDao的实现是什么了。如果实现是采用SpringHibernateTemplate的话你就去mock一个这样对象,不过此对象并不是接口,所以你要用到easymock的扩展包,以对它的支持。如果你实现的采用纯Hibernate的话。那你就去mock一个SessionFactory吧。很简单吧,难道这就是所谓的解耦吗?我想是的,这正是解耦。 哈哈

看下我的实现吧,采用Spring实现。
java 代码
 
  1. package org.wuhua.dao.impl;  
  2.   
  3. import java.util.Collection;  
  4.   
  5. import org.springframework.orm.hibernate3.support.HibernateDaoSupport;  
  6. import org.wuhua.dao.IBaseDao;  
  7.   
  8. public class BaseDao extends  HibernateDaoSupport  
  9. implements IBaseDao {  
  10.   
  11.     public void delete(Object o) {  
  12.          this.getHibernateTemplate().delete(o);        
  13.     }  
  14.   
  15.     public Collection list() {  
  16.        
  17.         return null;  
  18.     }  
  19.   
  20.     public Object save(Object o) {  
  21.         return this.getHibernateTemplate().save(o);  
  22.             
  23.     }  
  24.   
  25.     public Object update(Object o) {  
  26.         this.getHibernateTemplate().update(o);  
  27.         return o;  
  28.     }  
  29.   
  30. }  
测试代码
java 代码
 
  1. package org.wuhua.dao;  
  2.   
  3. import java.io.Serializable;  
  4.   
  5. import junit.framework.TestCase;  
  6.   
  7. import org.easymock.MockControl;  
  8. import org.easymock.classextension.MockClassControl;  
  9. import org.springframework.orm.hibernate3.HibernateTemplate;  
  10. import org.wuhua.dao.impl.BaseDao;  
  11.   
  12. public class BaseDaoTest extends TestCase {  
  13.   
  14.     MockControl control;  
  15.   
  16.     private HibernateTemplate ht;  
  17.   
  18.     private BaseDao baseDao;  
  19.   
  20.     protected void setUp() throws Exception {  
  21.         control = MockClassControl.createControl(HibernateTemplate.class);  
  22.         ht = (HibernateTemplate) control.getMock();  
  23.         baseDao = new BaseDao();  
  24.         baseDao.setHibernateTemplate(ht);  
  25.     }  
  26.   
  27.     public void testSave() {  
  28.         Object o = new Object();  
  29.         ht.save(o); 
  30.         //这里我是有疑问的。
  31.         //1,为什么HibernateTemplate返回的是Serializable。
  32.         //2,设置的返回植为什么一定要跟调用ht.save(o)一致呢?
  33.         control.setReturnValue(new Wuhua());  
  34.         control.replay();  
  35.         baseDao.save(o);  
  36.         control.verify();  
  37.     }  
  38.       
  39.     public void testUpdate() {  
  40.         Object a = new Object();  
  41.         ht.update(a);  
  42.        
  43.         control.replay();  
  44.         try {  
  45.             baseDao.update(a);  
  46.             fail("Not catch exception!");  
  47.         } catch(Exception e) {  
  48.                
  49.         }  
  50.         control.verify();  
  51.     }  
  52.       
  53.     class Wuhua implements Serializable {}  
  54.   
  55. }  

上面就是我第一次很认真的测试,有很多不明白的地方

里面对于测试Service大家是没有意义的,对于测试DAO层则表现各有各的看法。
比如

robbin     大哥建议:

测试DAO不如连数据库一起测试吧。因为DAO测试的目的不是DAO接口实现对不对,而是测试是否如你预期的发送了SQL,如你预期的返回了结果集。这个时候你Mock之后,测试就没有意义了。

hyysguyang      大哥建议:篇
wuhua 写道
分层的原因很多。这里我的看法片面就不说了
但对于mock来说是有莫大好处的。
比如service测试的时候完全可以做到隔离数据库,

我现在的意思是,
居然Service可以隔离Dao层,也就是说Dao层也是可以做到隔离相关的数据实现的。也是可以mock一个对象。而并非用实际的连接去代 替。如果我们的逻辑没出错的话,测试就算通过了,至于数据层的检测,那就不关我们的事情了,比如Hibernate由Hibernate去test, Spring由Spring去Test,Oracle由它自己去做。干自己的事情,别趟其他浑水。这样不是潇洒很多吗

但是数据库的测试毕竟比较特殊,记住测试的目的是确保你的代码质量,如果你确定你的这样测就没问题了,那无话可说,否则就尽量多的测试。
事实上,最原始的单元测试(plain testcase)就是用来测方法,测业务逻辑的,如果有逻辑就测,没逻辑就不用测了,同样的道理,相信你不会去测一个bean的get/set方法吧。
记住你测试的目的和动机,如果你认为测试dao层是为了测你的逻辑(你确定你的dao的实现代码是否真的存在逻辑),那你就mock吧,但是,我 们更相信,我们测DAO层,更应该是测访问数据库的情况,你如连接,sql是否正确,sequence是否正确等,而这些你必须要真正的连接数据库,也因 此,我们一般都是直接访问数据库来测试的,当然,如果可能你可以采用内存库。
事实上,我们对dao的测试,一般都进行所谓的的集成单元测试。我认为,你应该确定好你的测试策略,然后在去采用相应的测试方法。我在目前的开发中就是采用这样的方式测的。

上面两个大哥都建议测试DAO的时候还是连接数据库为好。
但个人认为上面两个大哥的单元测试以非纯正的单元测试了,而是集成单元测试。
其实说白了,测试这东西只是为了项目更好,更快的完成。至于是否要求纯单元,或者是集成单元测试,则看各位的需要,如果觉得集成单元测试对项目有帮助,那就用吧,现在发现对这个已经没有明显的界限了。


不理会它了,现在回归到我们用户注册的例子。

java 代码
 
  1. 1public interface IAccountDao extends IBaseDao {    
  2. 2.     public Account findAccountById(String id);    
  3. 3.     public Account findAccounByName(String name);    
  4. 4. }   


实际实现代码

java 代码
 
  1. package org.wuhua.dao.impl;  
  2.   
  3. import java.util.List;  
  4.   
  5. import org.wuhua.dao.IAccountDao;  
  6. import org.wuhua.model.Account;  
  7.   
  8. public class AccountDao extends BaseDao implements IAccountDao {  
  9.   public Account findAccountById(String id) {  
  10.         return (Account) this.getHibernateTemplate().get(Account.class, id) ;  
  11.     }  
  12.   
  13.     public Account findAccounByName(String name) {  
  14.         List l = this.getHibernateTemplate().find("from Account as a where a.name=?", name);  
  15.         if(l != null && l.size() >=1)  
  16.             return (Account) l.get(0);  
  17.         else   
  18.             return null;  
  19.     }  
  20. }  


java 代码
 
  1. package org.wuhua.dao;  
  2.   
  3. import java.util.ArrayList;  
  4. import java.util.List;  
  5.   
  6. import junit.framework.Assert;  
  7. import junit.framework.TestCase;  
  8.   
  9. import org.easymock.MockControl;  
  10. import org.easymock.classextension.MockClassControl;  
  11. import org.springframework.orm.hibernate3.HibernateTemplate;  
  12. import org.wuhua.dao.impl.AccountDao;  
  13. import org.wuhua.model.Account;  
  14.   
  15.    
  16.   
  17. public class AccountDaoTest extends TestCase {  
  18.       
  19.     private AccountDao accountDao;  
  20.     private org.springframework.orm.hibernate3.HibernateTemplate ht;  
  21.     private MockControl control;  
  22.   
  23.     protected void setUp() throws Exception {  
  24.         control = MockClassControl.createControl(HibernateTemplate.class);  
  25.         ht = (HibernateTemplate) control.getMock();  
  26.         accountDao = new AccountDao();  
  27.         accountDao.setHibernateTemplate(ht);  
  28.     }  
  29.   
  30.     protected void tearDown() throws Exception {  
  31.            
  32.     }  
  33.       
  34.     public void testFindAccountById(){  
  35.         Account a = new Account("wuhua");  
  36.         a.setId("10");     
  37.           
  38.         ht.get(Account.class, a.getId());  
  39.           
  40.         control.setReturnValue(a);  
  41.           
  42.         control.replay();  
  43.           
  44.         Account result =   accountDao.findAccountById(a.getId());  
  45.           
  46.         assertNotNull(result);  
  47.   
  48.         Assert.assertEquals(a.getId(),result.getId());  
  49.         Assert.assertEquals(a, result);  
  50.           
  51.         control.verify();  
  52.           
  53.     }  
  54.       
  55.     public void testFindAccountByName(){  
  56.         Account a = new Account("wuhua");      
  57.            
  58.         ht.find("from Account as a where a.name=?", a.getName());  
  59.         List l = new ArrayList();  
  60.         l.add(a);  
  61.         control.setReturnValue(l);  
  62.           
  63.         control.replay();  
  64.           
  65.         Account result =  accountDao.findAccounByName(a.getName());  
  66.   
  67.         Assert.assertEquals(a.getId(),result.getId());  
  68.         Assert.assertEquals(a, result);  
  69.           
  70.         control.verify();  
  71.           
  72.     }  
  73. }  

测试Service,因为Service依赖的Dao, 所以只需Mock一个Dao即可。在这里我详细的介绍关于注册这个功能的测试

java 代码
 
  1. public interface IAccountService extends IBaseService {  
  2.       Account findAccountById(String id);  
  3.       Account findAccounByName(String name);  
  4.       void regist(Account account) throws ObjectExistsException;  
  5. }  

   注册功能的实现。

java 代码
 
  1. public void regist(Account account) throws ObjectExistsException {  
  2.     if(accountDao.findAccounByName(account.getName()) != null)  
  3.         throw new ObjectExistsException("User's name is exists!");  
  4.       
  5.     accountDao.save(account);  
  6. }  


测试代码

java 代码
 
  1.     protected void setUp() throws Exception {  
  2.         control = MockControl.createControl(IAccountDao.class);  
  3.         accountDao = (IAccountDao) control.getMock();  
  4.         as = new AccountService();  
  5.         as.setAccountDao(accountDao);  
  6.     }  
  7.   
  8.   
  9. public void testFindAccountByName() {  
  10.         String name = "wuhua";  
  11.         accountDao.findAccounByName(name);  
  12.         Account a = new Account("wuhua");  
  13.         a.setId(name);  
  14.         control.setReturnValue(a);  
  15.         control.replay();  
  16.         Account at = as.findAccounByName(name);  
  17.         Assert.assertEquals(name, at.getId());  
  18.         Assert.assertEquals(a, at);  
  19.         control.verify();  
  20.     }  


首先我们建立一个关键字查询,name="wuhua";
然后调用Dao的方法,
然后自定义返回一个自己预期的对象,
最后通过比较这个对象判断结果是否是自己想要的

Action的测试是比较辛苦的。因为它依赖与其他的环境(比如tomcat)。
       在我的印象中,基于struts的测试是很麻烦的,因为对于execute方法,你必须mock两个对象进去。
      还好。基于Webwork的测试相对简单些。
      下面让我们来测试一个例子吧
      

java 代码
 
  1.       Account account;  
  2. IAccountService accountService;  
  3. public void setAccount(Account account) {  
  4.     this.account = account;  
  5. }  
  6.   
  7. public void setAccountService(IAccountService accountService) {  
  8.     this.accountService = accountService;  
  9. }  
  10.   
  11. public String regist() throws Exception {  
  12.     if(account == null) {  
  13.         account = new Account();  
  14.         return INPUT;  
  15.     }  
  16.       
  17.     if(!validForm(account))   
  18.         return INPUT;  
  19.       
  20.     try {  
  21.         accountService.regist(account);  
  22.     } catch (ObjectExistsException e) {  
  23.         e.printStackTrace();  
  24.         return INPUT;  
  25.     }  
  26.       
  27.     return SUCCESS;  
  28. }  
  29.   
  30. private boolean validForm(Account e) {  
  31.     if(e.getName() == null || e.getName().trim().equals(""))  
  32.         return false;  
  33.     if(e.getPassword() == null || e.getPassword().trim().equals(""))  
  34.         return false;  
  35.     return true;  
  36. }  


有经验的程序员见到上面的代码应该就知道怎么测试了。
我们只需setAccount,跟setAccountService即可,
而Account本身来讲就是是个po,所以可以自己new一个
AccountService则可以mock一个。真是太完美了,我太喜好mock,它总是给我惊喜

java 代码
 
  1. package org.wuhua.action;  
  2.   
  3. import junit.framework.TestCase;  
  4.   
  5. import org.easymock.MockControl;  
  6. import org.wuhua.exception.ObjectExistsException;  
  7. import org.wuhua.model.Account;  
  8. import org.wuhua.service.IAccountService;  
  9.   
  10. import sms.king.AccountManager;  
  11.   
  12. import com.opensymphony.xwork.Action;  
  13.   
  14. public class AccountActionTest extends TestCase {  
  15.     private MockControl control;  
  16.     IAccountService accountService;  
  17.     protected void setUp() throws Exception {  
  18.         control = MockControl.createControl(IAccountService.class);  
  19.         accountService = (IAccountService) control.getMock();  
  20.            
  21.     }  
  22.       
  23.     public void testRegistOk() throws Exception {  
  24.         Account employee = new Account("name");  
  25.         employee.setPassword("password");  
  26.           
  27.        
  28.            
  29.           
  30.         accountService.regist(employee);  
  31.         control.setVoidCallable(1);  
  32.           
  33.         control.replay();  
  34.           
  35.         AccountAction action = new AccountAction();  
  36.         action.setAccount(employee);  
  37.         action.setAccountService(accountService);  
  38.           
  39.         assertEquals(Action.SUCCESS, action.regist());  
  40.           
  41.         control.verify();  
  42.     }  
  43.       
  44.     public void testRegistNameExists() throws Exception {  
  45.         Account employee = new Account("name");  
  46.         employee.setPassword("password");  
  47.           
  48.    
  49.        
  50.           
  51.         accountService.regist(employee);  
  52.         control.setThrowable(new ObjectExistsException(""));  
  53.           
  54.         control.replay();  
  55.           
  56.         AccountAction action = new AccountAction();  
  57.         action.setAccount(employee);  
  58.         action.setAccountService(accountService);  
  59.           
  60.         assertEquals(Action.INPUT, action.regist());  
  61.           
  62.         control.verify();  
  63.     }  
  64. }  

 
ok,一个测试的例子就好了。

ronghao     2006-12-12 09:30

对ACTION我一般都是不测试的,因为它仅仅是页面的跳转而不包含逻辑

刑天战士     2006-12-12 09:30

struts的action测试最好用strutstestcase,不需要实现那么多东西了……

虽然以前用easymock测试过Dao,但那些Dao的实现,要么就hibernate,要么就用spring,而这两个框架的执行正确与否我们是不用关心的。JDBC是不是也这样测试了。答案是肯定的。
      这几天要用存储过程跟jdbc来做个项目,想想也有好长一段时间没用过JDBC来做项目了。该复习复习了。
      前阵子学了easymock,真好现在可以派上用场了。不过在测试的过程中还是遇到了不小问题,想来是自己基础不好的缘故。

       这次不TDD了,太麻烦了。
       先看看我们要测试的代码
     
java 代码
 
  1. CallableStatementcstmt = null;  
  2.         try {  
  3.             cstmt = _conn.prepareCall("{call LUCK_LOAD_COMMON(?,?)}");  
  4.             cstmt.setString(1"1");  
  5.             cstmt.registerOutParameter(2, java.sql.Types.VARCHAR);  
  6.   
  7.             cstmt.executeUpdate();  
  8.             return cstmt.getString(2);  
  9.   
  10.         } catch (Exception e) {  
  11.             GxDebug.logException(e);  
  12.             e.printStackTrace();  
  13.             return null;  
  14.         } finally {  
  15.             if (cstmt != null)  
  16.                 try {  
  17.                     cstmt.close();  
  18.                 } catch (Exception e) {  
  19.             }  
  20.         }  
  代码还挺长的。从上面的代码我们知道我们必须mock两个对象进去。一个是Connection, 一个是 CallableStatementcstmt 。
好再看看我们的测试代码
java 代码
  1. conn.prepareCall("{call LUCK_LOAD_COMMON(?,?)}");  
  2.     conControl.setReturnValue(cstmt);  
  3.     conControl.replay();  
  4.       
  5.     cstmt.setString(1"1");  
  6.     cstmt.registerOutParameter(2, java.sql.Types.VARCHAR);  
  7.     cstmt.executeUpdate();  
  8.     cstmtControl.setReturnValue(1);  
  9.     cstmt.getString(2);  
  10.     cstmtControl.setReturnValue("5,4,3");  
  11.     cstmt.close();  
  12.     cstmtControl.replay();  
  13.       
  14.       
  15.     String rusult = dao.getNumber();  
  16.     Assert.assertEquals("5,4,3", rusult);  
  17.       
  18.     conControl.verify();  
  19.     cstmtControl.verify();  

oh,my got!测试代码比实现代码还要多。这段代码能执行吗?
我想可以的。easymock的原理是记录-回放的模式。
我想要做的工作是:
1,记录你mock对象的工作记录,比如上面的代码我们mock对象的工作记录是:
java 代码
  1. conn.prepareCall("{call LUCK_LOAD_COMMON(?,?)}");  
  2.         conControl.setReturnValue(cstmt);  
  3.         cstmt.setString(1"1");  
  4.         cstmt.registerOutParameter(2, java.sql.Types.VARCHAR);  
  5.         cstmt.executeUpdate();  
  6.         cstmtControl.setReturnValue(1);  
  7.         cstmt.getString(2);  
  8.         cstmtControl.setReturnValue("5,4,3");  
  9.         cstmt.close();  
  10.   

 如果你工作记录的代码要求有返回值的话,那么你必须提供一个自定义的值给它,否则会报错。比如上面的
  •  cstmt.getString(2);  
  •         cstmtControl.setReturnValue("5,4,3");   //自己定义的返回值,用作以后的比较。
    上面的是记录操作,回放的时候,easymock会把记录的操作跟你实际的代码进行比较,如果里面出了什么差错,那么不好意思你的代码有问题,请修正后再测试。

    如果有兴趣可以自己试下。

经过前几篇的测试学习跟实践,我觉得有必要对这次学习做个总结。

以前我正常的设计流程是Database->Model -> Dao-> Service -> Action ->View。这样的设计伴随我1年多了,这样的设计方式好吗?这样的设计高效吗? 代码质量能保证吗? 我可以很肯定的回答,不能,如果数据库一该,我要在表格里面添加一个字段,或者什么的。那么它将会牵连到很多其他,修改的动作如下:Database->Model -> Dao-> Service -> Action ->View。 噢,my got,几乎跟设计的一样多,甚至更多,因为在修改的过程中你就算有再好IDE去重构它也不能保证它的正确性。然后你就要去测试,测试它的正确性。也许测试的过程将是修改过程的几倍时间。所以我个人觉得这样的设计方式是不高效的。总而言之就是这样的设计迟早会出问题的?

为什么会这样呢?难道就没有一种解决办法吗? 经过这段日子学习我发现,以前的设计不能很好的保证质量是因为你没有足够的单元测试去支撑着它,所以你改了代码后缺乏一个很好的手段去保持这段代码的质量,换句话的意思就是,没有一个静态的人去监督你的工作(我把单元测试比喻为静态的人,它只做一件事,就是督促你的代码不出问题)。

好了。我们已经找到了适合保证我们代码质量的方法了。但是我们还得提高我们的开发效率啊。这又怎么办呢?是不是还按照以前的方式吗?我想自己浑浑噩噩的活了20多年了。我想换种活法了,想找种更刺激,更有意义的生活方式。
设计有时候更生活是一样的,应该经常探索,经常实践才能感受的更多。
那好吧我们就来个彻底的变革吧。怎么变呢? 很明显:那就是TDD。
该怎么做呢?
以前的方式:Database->Model -> Dao-> Service -> Action ->View。
TDD的方式:Test->其他。
先看看下面Dao的例子吧:以前的方式:IBaseDao ->  BaseDao -> BaseDaoTest。
TDD:BaseDaoTest->IBaseDao->BaseDao.

 
  1. public void testFindAccountByName(){  
  2.     Account a = new Account("wuhua");      
  3.     ht.find("from Account as a where a.name=?", a.getName());  
  4.     List l = new ArrayList();  
  5.     l.add(a);  
  6.     control.setReturnValue(l);  
  7.     control.replay();  
  8.     Account result =  accountDao.findAccounByName(a.getName());  
  9.     Assert.assertEquals(a.getId(),result.getId());  
  10.     Assert.assertEquals(a, result);  
  11.     control.verify();  
  12.       
  13. }  
好,非常好,怎么这段代码不能运行呢?当然不行了,因为上面的很多类都没有。那我们这段代码的用途是什么呢?
用途就是:以为上面的从上面的代码我很清楚自己以后要做什么。1,要建立一个Model,里面起码有一个name属性,然后你会发现我们要测试的功能段是 accountDao.findAccounByName(a.getName()); 里面我们要求测试的SQL是 from Account as a where a.name=?, 意图明确吧。好,

我们写下实际代码吧。
java 代码
  1. public Account findAccounByName(String name) {  
  2.         List l = this.getHibernateTemplate().find("from Account as ", name);  
  3.         if(l != null && l.size() >=1)  
  4.             return (Account) l.get(0);  
  5.         else   
  6.             return null;  
  7.     }  
好,代码写好了。去运行我们的测试吧。结果是令人失望的。怎么会是红色的呢。肯定是逻辑代码出问题了(如果测试代码没问题的话)。经过检查发现原来 from Account as a where a.name=?跟 from Account as 完全两码事。好改回去

java 代码
 
  1. public Account findAccounByName(String name) {  
  2.         List l = this.getHibernateTemplate().
  3.                              find("from Account as a where a.name=?", name);  
  4.         if(l != null && l.size() >=2)  
  5.             return (Account) l.get(0);  
  6.         else   
  7.             return null;  
  8.     } 
怎么还是红色啊。我不干了(程序员暴躁的情绪出来了,我就经常这样)主管跟我说:“再查查吧。别灰心。”
后来查了半天发现原来
java 代码
 
  1. List l = new ArrayList();    
  2.  l.add(a);   
我只renturn一个预期的对象a
java 代码
 
  1. if(l != null && l.size() >=2)  
  2.             return (Account) l.get(0);  
  3.         else   
  4.             return null;  
而代码却要求我要传入预期两个对象才给我通过,所以代码只return null。

最后改了这段代码
java 代码
 
  1. public Account findAccounByName(String name) {  
  2.         List l = this.getHibernateTemplate().find("from Account
  3.                                         as a where a.name=?", name);  
  4.         if(l != null && l.size() >=1)  
  5.             return (Account) l.get(0);  
  6.         else   
  7.             return null;  
  8.     }  

终于通过了。绿色,绿色,我看到了。我对主管说。主管笑了。我也笑了

最后我郁闷下,写这篇文章足足话了我22个小时,
第一次写好了。杀毒突然重启。所以全没了。
第二次,提交不了。然后忘记备份,又全没了
第三次成功了。过程跟TDD差不多。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值