测试驱动开发with Junit(三)

第三章:使用mork进行测试开发:
       什么是mork?简单地说mork就是模型,模拟我们测试时需要的对象及测试数据。比如,用过Struts的朋友都知道,Struts中的action类要运行必须依靠服务器的支持,只有服务器可以提供HttpServletRequest,HttpServletResponse对象,如果不启动服务器,那么就没有办法对action类进行单元测试(当然了,使用mock测试除外)。对struts的Action进行测试是很困难的。即使当业务逻辑很好的被限定在业务层,Struts action通常还是会包含很重要的数据验证、数据转换和数据流控制代码。不对Struts action 支路进行测试在代码的覆盖率上会有很多的不足。单单依靠启动服务器运行程序来测试action太过于麻烦。如果让action脱离容器,那么测试就变得极为简单。脱离了容器,request与response对象如何获得?这时可以使用mork来模拟request与response对象,这样测试 action类就会变得极为简单。下面就简单的介绍一下如何使用StrutsTestCase来进行action的测试。

StrutsTestCase介绍
       StrutsTestCase工程提供了一种在JUnit框架下测试struts action的灵活、便利的方法。你可以通过设置请求参数,检查在Action被调用后的输出请求或Session状态这种方式对Struts Action做白盒测试。StrutsTestCase 提供了用框架模拟web容器的模拟测试方法也提供了真实的web容器(比如:Tomcat)下的测试方法。所有的StrutsTestCase单元测试类都源于模拟测试的 MockStrutsTestCase或容器内测试的CactusStrutsTestCase。
       下面的例子就是使用mock测试,因为它需要更少的启动和更快的运行。
要使用StrutsTestCase请在下列地址下载最新的发行包
http://sourceforge.net/project/showfiles.php?group_id=39190

      下面就开始我们的action测试之旅。首先先让我们了解一下要实现的程序的功能:在index.jsp中输入班级编号以列表形式在result.jsp页面中显示相关班级学员信息
在result.jsp中点击学员姓名在info.jsp中显示该学生的详细信息。
(注:这里数据库查询使用基本的jdbc实现,我们假设DAO层已经通过测试,因为这不是我们这里关注的重点,数据库使用SqlServer2000,同时假定你已经具备使用eclipse开发struts的相关知识)
       在pubs数据库中新建一个student表,在表中插入如下测试数据。

 
  1. use pubs        
  2. go        
  3.         
  4. create table student        
  5. ([id] int primary key identity(1,1),        
  6. stuName varchar(20) not null,        
  7. stuAge int not null,        
  8. stuSex varchar(20)not null,        
  9. stuClass varchar(20) not null       
  10. )        
  11. go        
  12.         
  13. insert into student values('jack',23,'man','GS2T11')        
  14. insert into student values('mohindar',24,'man','GS2T11')        
  15. insert into student values('tim',21,'man','GS2T11')        
  16. insert into student values('rose',23,'woman','GS2T11')        
  17. insert into student values('sakura',22,'woman','GS2T11')        
  18.      
  19. insert into student values('dess',23,'man','GS2T12')        
  20. insert into student values('hiruku',24,'man','GS2T12')        
  21. insert into student values('lucy',21,'woman','GS2T12')        
  22. insert into student values('liky',23,'woman','GS2T12')        
  23. insert into student values('desr',22,'woman','GS2T12')        
  24. go        


       先让我们来看看第一个功能如何实现:
  
       在search.jsp中填入数据,然后将其提交给SearchAction类来处理。SearchAction类调用业务逻辑层查询,将结果保存至 request对象的属性"result"中,并且跳转至info.jsp页面。如果查询不到结果,就构造一个ActionMessage,封装错误信息,并转向到error.jsp页面。
      下面的请求URL将会显示调用SearchAction类:
      search.do?method=search
      具体步骤如下:
      步骤1:在eclipse中放入strutstests-2.1.3.jar,junit.jar、commons-collections.jar
    
      步骤2:构造一个名为SearchForm的ActionForm和一个名为SearchAction 的action。SearchForm的属性为:clazzName

      步骤3:更改相应的配置文件

 
  1. <form-beans>  
  2.     <form-bean name="searchForm" type="com.struts.form.SearchForm" />  
  3. form-beans>  
  4.   
  5. <action-mappings>  
  6.     <action attribute="searchForm" input="/search.jsp"  
  7.         name="searchForm" parameter="method" path="/search" scope="request"  
  8.         type="com.struts.action.SearchAction" validate="false">  
  9.         <forward name="error" path="/error.jsp" />  
  10.         <forward name="success" path="/result.jsp" />  
  11.     action>  
  12. action-mappings>  

     步骤4:编写测试类
构造一个名为SearchActionTest的类并继承MockStrutsTestCase。在该类中添
加测试方法testSearchSuccess

 
  1. import servletunit.struts.MockStrutsTestCase;        
  2.        
  3. public class SearchActionTest extends MockStrutsTestCase        
  4. {        
  5.    public void testSearchSuccess()        
  6.    {        
  7.       setRequestPathInfo("/search.do");          //1行        
  8.       addRequestParameter("method""search");  //2行        
  9.       actionPerform();                          //3行        
  10.   }        
  11. }       


       代码解释:
       1行:设置请求SearchAction类的请求路径
       2行:在请求路径后加上一个请求参数
                1行与2行就构成了/search.do?method=search
       3行:调用actionPerform()来执行action类
       注:以上代码就是对一个测试方法的初始化。可以根据需要自行修改

       步骤5:根据struts-config.xml文件完善testSearch方法。先验证可以按照班级名称查询出所需要的数据,主要做以下事情:
       a)   初始化提交数据
       b)   验证request中"result"属性是否有值
       c)   是否返回result.jsp页面    
       代码如下:

 
  1. public class SearchActionTest extends MockStrutsTestCase        
  2. {        
  3.     public void testSearchSuccess()        
  4.     {        
  5.         setRequestPathInfo("/search.do");        
  6.         addRequestParameter("method""search");        
  7.              
  8.         addRequestParameter("clazzName","gs2t11");      //1行        
  9.         actionPerform();        
  10.              
  11.         assertNotNull(request.getAttribute("result"));  //2行        
  12.         verifyForward("success");                       //3行        
  13.     }        
  14. }      

       代码解释:
       1行:添加提交数据,该数据就相当与从search.jsp提交的数据
       2行:查询成功后,将结果放入request中,所以判断result中的"result"属性是否
                 有值来断定是否将结果成功放入request对象中。
       3行:验证查询成功后是否会返回success.jsp页面
       运行junit,如果出现以下错误(具体如下图)(请参考附件吧。这里所有的图都省略……)
       那么请在src下建立一个名WEB-INF的目录,将WebRoot下的WEB-INF中的web.xml文件与struts-config.xml文件拷贝过去即可。如图:


       步骤6:在SearchAction中用最简单的代码先去实现步骤5中的功能
       代码如下:
           

 
  1. public class SearchAction extends DispatchAction   
  2. {   
  3.     private SearchHandler sh;   
  4.   
  5.     public ActionForward search(ActionMapping mapping,ActionForm    
  6.               form, HttpServletRequest request, HttpServletResponse response)   
  7.     {   
  8.         SearchFormsearchForm = (SearchForm) form;   
  9.         //获取表单中输入的班级   
  10.         String className = searchForm.getClazzName();   
  11.            
  12.         //查询该班级的信息   
  13.         sh = new SearchHandler();   
  14.         List list=sh.searchByClassName(className);   
  15.            
  16.         if(list!=null)   
  17.         {   
  18.             //将班级信息保存至request中,并返回result.jsp   
  19.             request.setAttribute("result",list);   
  20.             return mapping.findForward("success");   
  21.         }   
  22.         else  
  23.             return null;   
  24.     }   
  25. }  

           注释比较详细,就不再多解释了
        运行测试类结果如下(成功部分编写完毕)

        步骤7:编写不能成功取出数据的测试。在SearchActionTest类中添加一个名为testSearchFail的方法
       代码如下:
      

 
  1. public void testSearchFail()   
  2. {   
  3.     setRequestPathInfo("/search.do");   
  4.     addRequestParameter("method""search");   
  5.            
  6.     addRequestParameter("clazzName","gs2txx");       //1行   
  7.     actionPerform();   
  8.            
  9.     verifyActionMessages(new String[] {"no.stuclass.found"});//2行   
  10.     verifyForward("error");                            //3行   
  11.            
  12.     verifyForwardPath("/error.jsp");                              //4行   
  13. }  

       代码解释:
       1行:输入一个不存在的班级
       2行:验证向ActionMessages中放入的消息文本
       3行:验证返回地址
       4行:验证页面地址是否与转向的error对应

      步骤8:更改SearchAction类,实现步骤7的功能
      代码如下:(粗体为增加部分)

 
  1. public class SearchAction extends DispatchAction   
  2. {   
  3.     private SearchHandler sh;   
  4.   
  5.     public ActionForward search(ActionMapping mapping,ActionForm    
  6.              form, HttpServletRequest request, HttpServletResponse response)   
  7.     {   
  8.         SearchFormsearchForm = (SearchForm) form;   
  9.         //获取表单中输入的班级   
  10.         String className = searchForm.getClazzName();   
  11.            
  12.         //查询该班级的信息   
  13.         sh = new SearchHandler();   
  14.         List list=sh.searchByClassName(className);   
  15.            
  16.         if(list!=null)   
  17.         {   
  18.             //将班级信息保存至request中,并返回result.jsp   
  19.             request.setAttribute("result",list);   
  20.             return mapping.findForward("success");   
  21.         }   
  22.         else  
  23.         {   
  24.              //构造一个ActionMessage保存一条消息   
  25.              ActionMessages messages=new ActionMessages();   
  26.              messages.add(Globals.MESSAGE_KEY, new ActionMess("no.stuclass.found")); 
                 saveMessages(request,messages);   
  27.              //返回error.jsp   
  28.              return mapping.findForward("error");   
  29.         }   
  30.     }   
  31. }  


       运行测试类,正确

       至此第一个功能测试开发完毕
       接着我们一起来完成第二个功能:
                      
       在result.jsp页面中,点击学生的姓名后将其提交给DisplayAction类来处理。DisplayAction类调用业务逻辑层查询,将结果保存至request对象的属性"stu"中,并且跳转至info.jsp页面
       下面的请求URL将会显示调用SearchAction类:
        display.do?method=display&stuName=jack
       具体步骤如下:
       步骤1:构造一个名为DisplayForm的ActionForm和一个名为DisplayAction的Action。DisplayForm的属性为stuName
       步骤2:修改相应的struts-config.xml

 
  1. <form-beans>  
  2.     <form-bean name="displayForm" type="com.struts.form.DisplayForm"/>  
  3. form-beans>  
  4.   
  5. <action-mappings>  
  6.     <action attribute="displayForm"    
  7.         name="displayForm" parameter="method" path="/display" scope="request"  
  8.         type="com.struts.action.DisplayAction" validate="false">  
  9.         <forward name="info" path="/info.jsp" />  
  10.     action>  
  11. action-mappings>  

       步骤3、编写DisplayAction的测试类DisplayActionTest,在其中加入一个名为testShowInfo()的方法。该方法完成以下事情:
       a) 初始化提交数据
       b) 验证request中"stu"属性是否有值
       c) 是否返回info.jsp页面
       代码如下:     

 
  1. public class DisplayActioTest extends MockStrutsTestCase   
  2. {   
  3.     public void testShowInfo()   
  4.     {   
  5.         setRequestPathInfo("/display.do");   
  6.         addRequestParameter("method","display");   
  7.         addRequestParameter("stuName""jack");   
  8.            
  9.         actionPerform();   
  10.            
  11.         assertNotNull(request.getAttribute("stu"));   
  12.         verifyForward("info");   
  13.            
  14.     }   
  15. }  

 

        步骤4:更改DisplayAction类,用最简短的代码实现其功能
       代码如下:

 
  1. public class DisplayAction extends DispatchAction   
  2. {   
  3.     private SearchHandler sh;   
  4.     public ActionForward display(ActionMapping mapping, ActionForm form,     
  5.                      HttpServletRequest request, HttpServletResponse response)   
  6.     {   
  7.         sh=new SearchHandler();   
  8.         DisplayForm displayForm = (DisplayForm) form;   
  9.         String name=displayForm.getStuName();   
  10.            
  11.         Student stu=sh.displayByName(name);   
  12.         request.setAttribute("stu",stu);   
  13.            
  14.         return mapping.findForward("info");   
  15.     }   
  16. }  

      
       步骤5:运行测试类,结果正确

       至此,第二个功能测试开发完毕
       经过前面两个功能的测试与运行,两个功能都可以单独测试通过,由于第二个功能是以第一个功能成功查询为前提才能运行,所以再编写一个测试,将两个 action功能整合。这样就可以测试整个功能模块是否正确。在DisplayActioTest中编写一个名为 testSearchAndDisplay()的方法来完成这个测试吧。

       代码如下:

 
  1. public void testSearchAndDisplay()   
  2. {   
  3.     //先调用SearchAction来取得数据   
  4.     setRequestPathInfo("/search.do");   
  5.     addRequestParameter("method""search");   
  6.            
  7.     addRequestParameter("clazzName","gs2t11");   
  8.     actionPerform();   
  9.            
  10.     List list=(List) request.getAttribute("result");   
  11.     Student student=(Student) list.get(0);   
  12.            
  13.     //调用DisplayAction类显示在result.jsp中选择的数据   
  14.     setRequestPathInfo("/display.do");   
  15.     addRequestParameter("method","display");   
  16.     addRequestParameter("stuName", student.getStuName());   
  17.            
  18.     actionPerform();   
  19.   
  20.     assertNotNull(request.getAttribute("stu"));   
  21.     verifyForward("info");   
  22. }  


        运行测试类,测试成功

        整个模块到这里全部开发测试完毕,但是还没完,作为测试驱动开发最重要的一环就是重构,这样才能保证你程序的健壮性、可读性、可维护性及优雅性。这里重构主要是针对Action类进行重构,你也可以用同样的方法在编写完测试代码之后对测试代码进行重构。这里主要针对以下几个地方进行重构
        1)  
抽取Action类中的变量:

               SearchAction类中:

 
  1. SearchForm searchForm= (SearchForm) form;     
  2. String className = searchForm.getClazzName();     
  3. sh = new SearchHandler();     
  4. List list=sh.searchByClassName(className);    


               可以用下列代码替换:

 
  1. sh = new SearchHandler();     
  2. List list=sh.searchByClassName(((SearchForm) form).getClazzName());    


                DisplayAction类中:

 
  1. DisplayForm displayForm = (DisplayForm) form;     
  2. String name=displayForm.getStuName();     
  3. Student stu=sh.displayByName(name);     
  4. request.setAttribute("stu",stu);    


               可以用下列代码替换:

 
  1. request.setAttribute("stu",sh.displayByName(((DisplayForm)form).getStuName()));    

        2)  抽取常量:

               SearchAction类中:

 
  1. request.setAttribute("result",list);      

               可以用下列代码替换

 
  1. private static final String RESULT = "result";     
  2. request.setAttribute(RESULT,list);    

              SearchAction类中

 
  1. request.setAttribute("stu",sh.displayByName(((DisplayForm) form).getStuName()));    

              可以用下列代码替换

 
  1. private static final String STUDENT = "stu";     
  2. request.setAttribute(STUDENT,sh.displayByName(((DisplayForm) form).getStuName()));    

        构造一个名为BaseAction的类,继承DispatchAction类,并在其中初始化一个SearchHandler对象

 
  1. public class BaseAction extends DispatchAction     
  2. {     
  3.     protected SearchHandler sh=new SearchHandler();     
  4. }    


        SearchAction类与DisplayAction类可改为

 
  1. public class SearchAction extends BaseAction     
  2. public class DisplayAction extends BaseAction    


        同时要去掉两个类中的SearchHandler属性

        SearchAction类中下列代码可以抽取成方法:

 
  1. ActionMessages messages=new ActionMessages();     
  2. messages.add(Globals.MESSAGE_KEY, new ActionMessage("no.stuclass.found"));     
  3. saveMessages(request,messages);    


        可以用下列代码替换:

 
  1. addResultMessage(request,"no.stuclass.found");   
  2.   
  3. private void addResultMessage(HttpServletRequest request,String message)   
  4. {   
  5.      ActionMessages messages=new ActionMessages();   
  6.      messages.add(Globals.MESSAGE_KEY, new ActionMessage("no.stuclass.found"));   
  7.      saveMessages(request,messages);   
  8. }  


       由于在别的Action中也要经常生成ActionMessages对象,所以再将该方法 抽取到BaseAction中。

       最后BaseAction类代码如下:

 
  1. public class BaseAction extends DispatchAction   
  2. {   
  3.     protected SearchHandler sh=new SearchHandler();   
  4.   
  5.     protected void addResultMessage(HttpServletRequest request, String message)   
  6.     {   
  7.         ActionMessages messages=new ActionMessages();   
  8.         messages.add(Globals.MESSAGE_KEY, new ActionMessage(message));   
  9.         saveMessages(request,messages);   
  10.     }   
  11. }  

        最后代码效果请参考源代码。

        4)    重构注意事项:

               a) 每次要一点点的重构,万万不要一次重构很多地方。

               b) 每重构完一个地方要运行测试类来保证其正确性。

               c) 不要怕麻烦。在后期重构的优势就会越来越明显。

       到此为止,全部的测试与代码编写已经结束。还等什么,将已经做好的页面套上去吧,感受一下不用启动服务器来开发web应用程序的乐趣吧,感受测试驱动开发给你带来的开发思想的质的飞跃……

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值