单元测试之初步实践

 断断续续地学习了一些单元测试的知识,在最近的编码过程中有意识地进行了实践,勉强能达到一点测试的既定目的,但感觉疑惑仍然不少。

在javaeye上也拜读了诸多高人们关于单元测试、TDD方面的文章,获益良多,但是感觉很多文章起点有些高,像我这样比较笨的人读多次都不一定能领悟,适合入门一级的测试文章不太多。因此我想将自己实施单元测试的一些实践整理出来,尽量表述出我的想法,尽量提供比较详细的代码,希望初次接触单元测试的朋友能从中受益,从而少走一些弯路。另外,我在学习和实施单元测试的过程中也有很多不解和困惑,希望可以得到大家的指点。

 先列出一个测试代码实例吧。
 业务逻辑:对员工信息的增删改查;
 业务对象:EmpBO;
 业务对象接口代码(部分):

java 代码
  1. /**     
  2.  * Title:员工BO接口    
  3.  * Description:维护员工信息    
  4.  * @author xhl     
  5.  * @version 1.0     
  6.  * @date 2007-9-18     
  7.  */      
  8. public interface EmpBOI {       
  9.  /**新增一条员工信息     
  10.   * @param fmCau 员工form     
  11.   * @return ArrayList     
  12.   * 0 String true/false     
  13.   * 1 String 成功/失败信息     
  14.   * @author xhl     
  15.   * @date 2007-9-29     
  16.   */      
  17.  public ArrayList saveNewEmp(CcpAclsUserExtForm fmCau);       
  18.       
  19.  /**修改员工信息     
  20.   * @param fmCau 员工form     
  21.   * @return ArrayList     
  22.   * 0 String true/false     
  23.   * 1 String 成功/失败信息     
  24.   * @author xhl     
  25.   * @date 2007-9-29     
  26.   */      
  27.  public ArrayList saveEmp(CcpAclsUserExtForm fmCau);       
  28.         
  29.  /**删除员工信息     
  30.   * @param ccpUserGuid 员工guid     
  31.   * @return ArrayList     
  32.   * 0 String true/false     
  33.   * 1 String 成功/失败信息     
  34.   * @author xhl     
  35.   * @date 2007-9-18     
  36.   */      
  37.  public ArrayList removeEmp(String ccpUserGuid);       
  38.         
  39.  /**根据员工guid取员工form     
  40.   * @param ccpUserGuid 员工guid     
  41.   * @return ArrayList     
  42.   * CcpAclsUserExtForm     
  43.   * @author xhl     
  44.   * @date 2007-9-29     
  45.   */      
  46.  public ArrayList getEmpById(String ccpUserGuid);       
  47.         
  48. }     

 这段代码反映了典型的CRUD业务逻辑操作,对实现类的测试基本说明了我在实践单元测试时遇到的问题。
 对于写代码的顺序,我一般是先根据概设文档定义出业务逻辑操作类接口,测试类,业务逻辑类,然后针对每一个接口方法,先写该方法的测试用例,然后在业务逻辑类中实现该方法,运行测试,修改或重构实现代码。
 下面来看测试代码。
 首先定义了一个测试基类,扩展自Spring提供的JUnit封装类AbstractDependencyInjectionSpringContextTests。
 测试基类代码:  

java 代码
  1. public abstract class SpringUnitTest extends AbstractDependencyInjectionSpringContextTests{       
  2.         
  3.   /* (non-Javadoc)     
  4.    * @see org.springframework.test.AbstractSingleSpringContextTests#getConfigLocations()     
  5.    */      
  6.   protected String[] getConfigLocations() {       
  7.    // TODO Auto-generated method stub       
  8.    setAutowireMode(AUTOWIRE_BY_NAME);       
  9.    return new String[]{Constants.DEFAULT_SPRING_CONTEXT_HIB,       
  10.      Constants.DEFAULT_SPRING_CONTEXT_SER,       
  11.      Constants.DEFAULT_SPRING_CONTEXT_RES,       
  12.      Constants.DEFAULT_TEST_SPRING_CONTEXT_CCP};       
  13.   }       
  14.  }   

 SpringUnitTest类扩展AbstractDependencyInjectionSpringContextTests,实现了其抽象方法getConfigLocations,将Spring对bean的匹配方式设为AUTOWIRE_BY_NAME,根据名称而非type查找bean,然后返回Spring相关 配置文件的路径。配置文件中定义了DataSource数据源,测试代码运行时连接数据库。
 对员工管理BO的测试类EmpBOTest定义:

java 代码
  1. public class EmpBOTest extends SpringUnitTest {       
  2.   protected EmpBOI empBO;       
  3.         
  4.   public void setEmpBO(EmpBOI empBO) {       
  5.    this.empBO = empBO;       
  6.   }       
  7.  }     

 测试类中注入了待测试的业务类。

首先,写新增员工方法saveNewEmp的测试代码:

java 代码
  1. public void testSaveNewEmp(){       
  2.     CcpAclsUserExtForm fmCau = new CcpAclsUserExtForm);                                 
  3.     fmCau.setCauGuid("9DDC036A177088F0FAE833CEA0971DF0");       
  4.     fmCau.setName("吕南");       
  5.     fmCau.setSex("02");       
  6.     fmCau.setBirth("1989-11-24");       
  7.     fmCau.setAddress("北京市大兴区");       
  8.     fmCau.setMobile("13810384254");       
  9.     fmCau.setWorkUnit("威天软件");       
  10.     fmCau.setTel("01055669235");       
  11.     ArrayList retlist = this.ccpUserBO.saveNewEmp(fmCau);       
  12.     assertEquals("true",(String)retlist.get(0));       
  13. }    

 

然后在业务实现类EmpBO中具体实现该方法的业务逻辑。

说明一下,CcpAclsUserExtForm是对员工pojo对象的封装,员工对象的主键是cauGuid,这是一个由程序依据一定规则随机产生的32位字符串。

OK,现在业务代码有了,测试代码也已就位,数据库已运行,配置文件也在正确路径上,一切准备工作均已就绪,可以运行测试了。

Run测试代码,发现bug,修改业务代码,再次run,直到绿条出现,一个方法测试完成,一路有惊无险,很顺利。

当然上面的测试代码只测试了程序正常执行的分支,没有覆盖其它可能出现异常的分支,完全可以加入对异常分支的测试代码,不过这个业务比较简单,对正常分支测试通过基本就OK了。个人觉得,单元测试没有必要太看重测试覆盖率,够用就行了。

这样,测试类中就有了一个测试方法了:

java 代码
  1. public class EmpBOTest extends SpringUnitTest {       
  2.     protected EmpBOI empBO;       
  3.       
  4.     public void setEmpBO(EmpBOI empBO) {       
  5.         this.empBO = empBO;       
  6.     }       
  7.       
  8.     public void testSaveNewEmp(){          
  9.     CcpAclsUserExtForm fmCau = new CcpAclsUserExtForm);                                   
  10.     fmCau.setCauGuid("9DDC036A177088F0FAE833CEA0971DF0");          
  11.     fmCau.setName("吕南");          
  12.     fmCau.setSex("02");          
  13.     fmCau.setBirth("1989-11-24");          
  14.     fmCau.setAddress("北京市大兴区");          
  15.     fmCau.setMobile("13810384254");          
  16.     fmCau.setWorkUnit("威天软件");          
  17.     fmCau.setTel("01055669235");          
  18.     ArrayList retlist = this.ccpUserBO.saveNewEmp(fmCau);          
  19.     assertEquals("true",(String)retlist.get(0));          
  20.     }          
  21. }     

测试代码运行后,数据库中就有了员工信息记录。接下来是实现员工信息的修改逻辑。测试代码如下:

java 代码
  1. public void testSaveEmp(){       
  2.     CcpAclsUserExtForm fmCau = new CcpAclsUserExtForm();       
  3.     fmCau.setCauGuid("FD705E2FA08E95956241040BE3D83D69");       
  4.     fmCau.setName("吕南修改");       
  5.     fmCau.setSex("02");       
  6.     fmCau.setBirth("1968-10-15");       
  7.     fmCau.setAddress("北京市昌平区");       
  8.     fmCau.setMobile("13999999999");       
  9.     fmCau.setWorkUnit("join-cheer");       
  10.     fmCau.setTel("01058561199");       
  11.     ArrayList retlist = this.ccpUserBO.saveEmp(fmCau);       
  12.     assertEquals("true",(String)retlist.get(0));               
  13. }     

然后实现具体业务逻辑代码,运行测试,OK,绿条出现,测试通过。完事大吉了吗?

貌似没有问题,但是上面的测试代码实际是很脆弱。请注意这一句:

fmCau.setCauGuid("FD705E2FA08E95956241040BE3D83D69");

这是设置待修改的员工主键,问题就在这儿。主键是随机产生的,每个员工的主键值都是不同的。此处我将主键写死,该主键代表运行新增员工的测试代码testSaveNewEmp得到的员工记录。那么,我下次运行新增员工方法testSaveNewEmp时,得到的员工记录主键发生了变化,为了使修改员工的测试方法testSaveEmp正确运行,我发布修改测试方法中的代码,将待修改的员工对象主键值设为这次得到的员工记录的主键值。

如此一来,我的测试类无法运行,只能每次运行其中的一个测试方法。这种笨方法在业务逻辑开发阶段还能承受,但测试的自动化运行就没办法了。测试结果无法再现,每次运行都要修改测试代码,太恐怖了!

经过N久的痛苦之后,我决定改变测试基类。现在的基类扩展自AbstractDependencyInjectionSpringContextTests,我将其改为继承自AbstractTransactionalDataSourceSpringContextTests,这样我的测试类就自动具有事务功能,即在每个测试方法执行后Spring会自动回滚事务,将数据库还原为初始状态。然后在我的测试类里定义一个私有方法,用于向数据库中插入测试数据,每个测试方法运行时都要调用这个方法产生数据,因为测试类具有自动回滚功能,每个方法运行完成后,数据库会还原,将测试数据清空,以待下次运行时插入。这样一来,每次测试类运行时,可以保证测试数据都是相同的。这样我就可以随时运行测试而不用担心修改测试代码了。

修改后的测试基类如下:

java 代码
  1. public class SpringTransactUnitTest extends     
  2.         AbstractTransactionalDataSourceSpringContextTests {      
  3.      
  4.     /* (non-Javadoc)    
  5.      * @see org.springframework.test.AbstractSingleSpringContextTests#getConfigLocations()    
  6.      */     
  7.     protected String[] getConfigLocations() {      
  8.         // TODO Auto-generated method stub      
  9.         setAutowireMode(AUTOWIRE_BY_NAME);      
  10.         return new String[]{Constants.DEFAULT_SPRING_CONTEXT_HIB,      
  11.                 Constants.DEFAULT_SPRING_CONTEXT_SER,      
  12.                 Constants.DEFAULT_SPRING_CONTEXT_RES,      
  13.                 Constants.DEFAULT_TEST_SPRING_CONTEXT_CCP,      
  14.                 Constants.DEFAULT_TEST_SPRING_CONTEXT_APP      
  15.         };      
  16.     }      
  17. }    

修改后的测试类如下:

java 代码
  1. public class EmpBOTest extends SpringTransactUnitTest {          
  2.     protected EmpBOI empBO;          
  3.          
  4.     public void setEmpBO(EmpBOI empBO) {          
  5.         this.empBO = empBO;          
  6.     }       
  7.           
  8.     private ArrayList add(){      
  9.     CcpAclsUserExtForm fmCau = new CcpAclsUserExtForm);                                     
  10.     fmCau.setCauGuid("9DDC036A177088F0FAE833CEA0971DF0");             
  11.     fmCau.setName("吕南");             
  12.     fmCau.setSex("02");             
  13.     fmCau.setBirth("1989-11-24");             
  14.     fmCau.setAddress("北京市大兴区");             
  15.     fmCau.setMobile("13810384254");             
  16.     fmCau.setWorkUnit("威天软件");             
  17.     fmCau.setTel("01055669235");             
  18.     ArrayList retlist = this.ccpUserBO.saveNewEmp(fmCau);             
  19.     return retlist;   
  20.     }      
  21.          
  22.     public void testSaveNewEmp(){             
  23.     ArrayList retlist = add();      
  24.     assertEquals("true",(String)retlist.get(0));             
  25.     }             
  26.      
  27.     public void testSaveEmp(){      
  28.     add();      
  29.     CcpAclsUserExtForm fmCau = new CcpAclsUserExtForm();      
  30.     fmCau.setCauGuid("9DDC036A177088F0FAE833CEA0971DF0");      
  31.     fmCau.setName("王小二");      
  32.     ArrayList retlist = this.ccpUserBO.saveEmp(fmCau);      
  33.     assertEquals("true",(String)retlist.get(0));      
  34.     }      
  35. }   


业务类中的删除与查询方法就不在此赘述了。
一个小技艺:私有方法add用于产生测试数据,其实也可能通过AbstractTransactionalDataSourceSpringContextTests提供的jdbcTemplate执行原生SQL语句来向数据库中插入数据。这在测试业务类没有提供新增方法时非常实用。

另外,我认为,测试时最好使用一个单独的测试库,不要用项目组公用的开发库,否则会产生很多麻烦。

以上是我在具体项目中实施单元测试的一点心得体会,贴出来给大家参考,也欢迎大家提出批评意见。

猜你喜欢

转载自hlxiong.iteye.com/blog/134537