TDD with JUnit

测试驱动开发(TDD)是极限编程(XP)的重要特点,它是以持续性的测试来推动代码的开发,即可以简化代码,又可以保证质量。它改变了先编写代码,后编写测试,而是先编写测试,然后在编写代码来满足测试的方法。这样使得测试工作不仅仅是单纯的测试,而成为了设计的一部分。对于刚 入门的编程者来说,也许觉得非常地别扭,但是当你习惯了这种编程方式之后,你会发现,TDD会成为你的得力助手。
      下面的内容就以 学习JUnit这个测试工具来简单的介绍TDD。(注:本文主要内容是介绍JUnit的使用,顺便抛砖引玉介绍TDD的开发过程。想了解更多关于TDD,可以参考其他的一些专业书籍)。
      开发环境是:Eclipse3.2,已经集成了JUnit,所以就对Junit的配置不做介绍(网上这种文章太多了)。

第一章:JUnit快速入门:
       需求:需要一个工具类,可以实现2个数的加、减、乘、除四个功能。我们给这个类起名叫CalculateUtil。
       先编写测试代码:



   1. import junit.framework.TestCase;                //1行   
   2. public class CalculateUtilTest extends TestCase  //2行   
   3. {   
   4.     public void testCreate()                     //3行   
   5.     {   
   6.         new CalculateUtil();   
   7.     }   
   8. }  

      代码解释:
      1行:导入JUnit必须的类
      2行:任何一个测试类必须集成TestCase类。
                测试类的类名命名   方式:被测试类类名+Test。这是一种比较好的习惯,
                比如从CalculateUtilTest这个名称就知道被测试类是CalculateUtil
       3行:测试类的测试方法。所有的测试方法都必须以test单词开头,并且方法的返
                回类型为void。
       编写完测试代码之后在Eclipse中运行该测试类,发现Junit运行出错(显示了一条红色杠)

       这是在预料之中,因为我们还没有编写CalculateUtil类。为了使测试通过,那么下面开始编写CalculateUtil类。CalculateUtil类代码如下:



   1. public class CalculateUtil   
   2. {   
   3. }  


        然后再次运行测试类。这时会发现测试成功。
      


   1. import junit.framework.TestCase;   
   2. public class CalculateUtilTest extends TestCase   
   3. {   
   4.     public void testCreate()                    //1行   
   5.     {   
   6.         CalculateUtil ca=new CalculateUtil();      
   7.         assertEquals(5, ca.add(3,2));             //2行   
   8.     }   
   9. }  


       在这里代码稍微改变了一下,使用ca引用来指向生成的CalculateUtil对象。
       代码解释:
       1行:测试类的方法必须以单词test开头
       2行:assertEquals()方法。以assert单词开头的方法就是JUnit测试框架中的断   
                言方法。比如assertEquals(5, ca.add(3,2)); 方法就是断言ca的add(3,2)方
                法返回的结果等于5。如果add(3,2)返回5那么测试成功,否则测试失败。
         运行测试类,这时测试失败。那么向CalculateUtil中添加代码来保证测试成功:



   1. public class CalculateUtil   
   2. {   
   3.     public int add(int numOne,int numTwo)   
   4.     {   
   5.         return numOne+numTwo;   
   6.     }   
   7. }  


       为了验证add方法是否是真的返回两个数的相加的结果,我们继续在添加一行测试代码:



   1. assertEquals(7, ca.add(4,2));  

    再运行会发现测试出错
      故障跟踪中错误描述为:
      junit.framework.AssertionFailedError:expected:<7> but was <6>
      (如果你英语不是很差的话,相信这句话不用我解释也明白了)。在测试的时候,要选用一些容易出错的边界值进行测试,有正确的测试用例也要有错误的测试用例。
     在完成add方法之后,下面开始进行除法的编写。首先编写测试代码:



   1. import junit.framework.TestCase;   
   2. public class CalculateUtilTest extends TestCase   
   3. {   
   4.     public void testCreate()   
   5.     {   
   6.         CalculateUtil ca=new CalculateUtil();   
   7.         assertEquals(5, ca.add(3,2));               
   8.     }   
   9.     public void testDivision()   
  10.     {   
  11.         CalculateUtil ca=new CalculateUtil();   
  12.         assertEquals("两个数相除", 2,ca.division(4, 2));  //1行   
  13.     }   
  14. }  


      代码解释:
      1行:assertEquals("两个数相除", 2,ca.division(4, 2));和前面assertEquals方法  
               不一样,该方法多了一个参数,这个参数就是对该测试的一个简单的描述。
       如果测试失败,那么会有提示。
       为了使测试通过,就要在CalculateUtil类中添加一个division方法。代码如下:


   1. public class CalculateUtil   
   2. {   
   3.     public int add(int numOne,int numTwo)   
   4.     {   
   5.         return numOne+numTwo;   
   6.     }   
   7.     
   8.     public int division(int numOne,int numTwo)   
   9.     {   
  10.         return numOne/numTwo;   
  11.     }   
  12. }  


        运行测试代码,测试通过。那么这样就可以了吗?由于除法非常特殊,如果除数为0那么会怎么样呢?我们继续在testDivision()中添加一行测试代码测试除数为0的情况:



   1. public void testDivision()   
   2. {   
   3.     CalculateUtil ca=new CalculateUtil();   
   4.     assertEquals("两个数相除", 2,ca.division(4, 2));   
   5.     assertEquals("除数为0:", 2,ca.division(4,0));   
   6. }  


       运行测试,会发现testDivision()方法抛出了异常。

       测试表明:当除数为0的时,方法division()生成了一个异常。那么我们希望在测试用例中捕获该异常。更改后的testDivision()方法如下:



   1. public void testDivision()   
   2. {   
   3.     CalculateUtil ca=new CalculateUtil();   
   4.     assertEquals("两个数相除:", 2,ca.division(4,2));   
   5.     try  
   6.     {   
   7.         assertEquals("除数为0:", 2,ca.division(4,0));   
   8.         fail("除数不为0");                          //1行   
   9.     }   
  10.     catch(ArithmeticException ex){}   
  11. }  

      代码解释:fail()一旦被执行,会立即中止测试, java 虚拟机不再执行任何别的代码,并且会抛出 junit.framework.AssertionFailedError异常。结合上面的try-catch语句,如果产生异常,那么会忽略fail ()方法,如果没有产生异常那么就会调用fail()方法使测试失败,用这种方式就可以测试是否有异常产生,如果抛出异常,那么测试通过,否则测试失败。再次运行测试用例,一条绿杠显示测试通过。
       但是这样还是不妥,因为类CalculateUtil毕竟是不知道测试类的存在,虽然在测试类中用异常捕获,但是一旦脱离测试类,那么division()方法的健壮性还是受到质疑。所以从新修改division()方法



   1. public int division(int numOne,int numTwo)throws ArithmeticException   
   2. {   
   3.     return numOne/numTwo;   
   4. }  


       再重新运行测试用例,测试通过。
注:处理异常的方式很多,这里只是其中一种做法。也可以在division()方法中写一段业务逻辑,判断numTwo是否为0,如果为0则抛出异常等……,在这里不做深入讨论。
        从刚才的几个例子中,可以大概的知道TDD的开发思路,也知道JUnit的基本用法。但当我们反观CalculateUtilTest 类中的测试方法可以发现:类中的两个测试方法都构造了CalculateUtil的对象ca,但是对于程序来说,我们只需要构造一个对象即可。我们可以对 CalculateUtilTest类中的代码稍作修改。修改后代码如下:



   1. public class CalculateUtilTest extends TestCase   
   2. {   
   3.     private CalculateUtil ca;   
   4.     protected void setUp() throws Exception    //1行   
   5.     {   
   6.         ca=new CalculateUtil();                    
   7.     }   
   8.     
   9.     public void testCreate()   
  10.     {   
  11.         assertEquals(5, ca.add(3,2));   
  12.         assertEquals(7, ca.add(4,2));   
  13.     }   
  14.     
  15.     public void testDivision()   
  16.     {   
  17.         CalculateUtil ca=new CalculateUtil();   
  18.         assertEquals("两个数相除:", 2,ca.division(4,2));   
  19.         try  
  20.         {   
  21.             assertEquals("除数为0:", 2,ca.division(4,0));   
  22.             fail("除数不为0");                           
  23.         }   
  24.         catch(ArithmeticException ex){}   
  25.     }   
  26. }  


       代码解释:
       1行:JUnit在执行每个测试之前都先执行setUp方法,可以将公共的测试初始化代码放在setUp方法中。与setUp功能方法相反的是 protected void tearDown() throws Exception方法;可以将测试结束后要执行的收尾工作放入tearDown方法中,比如:关闭数据库连接,关闭流……
第二章:JUnit测试套件:
       在第一节中简单的介绍了JUnit的使用,但是每次运行测试类时,该测试类的所有方法全部都被测试一遍,如果想单独测试某个方法还是比较麻烦的。但是可以利用测试套件来解决这个问题。
       下面我们先更改CalculateUtilTest向里面增加一个构造方法


   1. import junit.framework.TestCase;   
   2. public class CalculateUtilTest extends TestCase   
   3. {   
   4.     
   5.     public CalculateUtilTest(String name)    //1行   
   6.     {   
   7.         super(name);   
   8.     }   
   9.     
  10.  ……其余方法省略   
  11. }  


       代码解释:
       1行:自定义构造方法,里面有一个String参数,在该方法中调用父类的构造方
                 法。
       构造一个类来操作测试套件:

   1. import junit.framework.*;   
   2. public class MainTest   
   3. {   
   4.     public static Test suite()  //1行   
   5.     {   
   6.         TestSuite suite = new TestSuite();  //2行   
   7.      
   8.         //添加测试testDivision方法   
   9.         suite.addTest(new CalculateUtilTest("testDivision"));  //3行   
  10.      
  11.         //添加测试testCreate方法   
  12.         suite.addTest(new CalculateUtilTest("testCreate"));   
  13.         return suite;   
  14.     }   
  15.     
  16.     public static void main(String[] args)   
  17.     {   
  18.         //执行测试   
  19.         junit.textui.TestRunner.run(suite());   //4行   
  20.     }   
  21. }   

       代码解释:
       1行:静态方法,返回Test类对象。该方法主要是构造TestSuite类对象,然后向
                其中加入你想要测试的方法。
       2行:构造TestSuite类对象
       3行:向TestSuite对象中添加一个要测试的方法。
                 new CalculateUtilTest("testDivision")表示测试CalculateUtilTest类的
                 testDivision方法。
       4行:运行该测试套件
       上面的方法可以非常方便地添加自己所需要的方法,如果是用该方法,就要在编写测试方法时将其加入测试套件中。如果你觉得很麻烦,那么就使用下列一行代码一次测试一个类吧。

   1. public static void main(String[] args)   
   2. {   
   3.     junit.swingui.TestRunner.run(CalculateUtilTest.class);   
   4. }  


        或者在suite方法中添加所要测试的类也可以

   1. public static Test suite()   
   2. {   
   3.     TestSuite suite = new TestSuite();   
   4.     suite.addTestSuite(CalculateUtil.class);   
   5.     return suite;   
   6. }  

       尽管上面的测试套件提供了你所需要的各种粒度测试方法(按照方法名,测试整个类),但是有个问题是,当我们面临一大堆测试代码时,很容易忘记将你的测试类加入到一个测试套件中去。一个好的解决方法是让java程序扫描你的classpath中的全部类,搜集所需要的测试类然后逐一执行。这样做的好处就是不会有测试被遗漏,但缺点是某些测试,你并不希望每次都运行它们。具体做法如下:先编写收集测试类的功能,并将全部测试类装入测试套件。代码如下

   1. import java.lang.reflect.Modifier;   
   2. import java.util.*;   
   3. import junit.runner.*;   
   4. import junit.framework.*;   
   5. public class SuiteBuilder   
   6. {   
   7.     //将所有的测试类都加入TestSuite中   
   8.     public  Test suite()   
   9.     {   
  10.         TestSuite suite=new TestSuite();   
  11.      
  12.         List list=getTestClassNames();   
  13.         for(int i=0;i
  14.         {   
  15.             suite.addTestSuite(createClass(list.get(i).toString()));   
  16.         }   
  17.         return suite;   
  18.     }   
  19.     
  20.     //获取全部测试类的类名   
  21.     public List   getTestClassNames()   
  22.     {   
  23.         TestCollector collector = new ClassPathTestCollector()   
  24.         {   
  25.             public boolean isTestClass(String classFileName)   
  26.             {   
  27.                 if (!super.isTestClass(classFileName))   
  28.                      return false;   
  29.                 String className = classNameFromFile(classFileName);   
  30.                 Class clazz = createClass(className);   
  31.                 return TestCase.class.isAssignableFrom(clazz) && isConcrete(clazz);   
  32.             }   
  33.         };   
  34.         return Collections.list(collector.collectTests());   
  35.     }   
  36.     //判断该类是否是接口或抽象类   
  37.     private boolean isConcrete(Class clazz)   
  38.     {   
  39.         if (clazz.isInterface())   
  40.             return false;   
  41.         int modifiers = clazz.getModifiers();   
  42.        return !Modifier.isAbstract(modifiers);   
  43.      }   
  44.      //加载该类   
  45.     private Class createClass(String name)   
  46.     {   
  47.         try  
  48.         {   
  49.             return Class.forName(name);   
  50.         }   
  51.         catch (ClassNotFoundException e)   
  52.         {   
  53.             return null;   
  54.         }   
  55.      }   
  56.     //判断该类是否是测试类:标准如下   
  57.     //1:类文件以.class结束   
  58.     //2:类名不能有$,防止内部类   
  59.     //3:类名必须以Test单词结尾   
  60.     protected boolean isTestClass(String classFileName)   
  61.     {   
  62.         return classFileName.endsWith(".class") && classFileName.indexOf('$') < 0 &&     
  63.                                   classFileName.indexOf("Test") > 0;   
  64.     }   
  65. }   

       注:注释都比较详细,在这里就不多解释了。
       然后编写测试套件类:

   1. package com;   
   2. public class MainTest   
   3. {   
   4.     public static void main(String[] args)   
   5.     {   
   6.         junit.textui.TestRunner.run(new SuiteBuilder().suite());   
   7.     }   
   8. }  


       运行该测试套件,你会发现在你的classpath下的所有测试类都会被执行。只不过是文本形式显示结果。
第三章:使用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应用程序的乐趣吧,感受测试驱动开发给你带来的开发思想的质的飞跃……
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值