`

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应用程序的乐趣吧,感受测试驱动开发给你带来的开发思想的质的飞跃……
分享到:
评论

相关推荐

    测试驱动开发with Junit(三)

    在"测试驱动开发with Junit(三)"这个主题中,我们可以深入探讨以下知识点: 1. **测试驱动开发(TDD)的基本原则**: - **红-绿-重构**:首先编写失败的测试(红),然后写代码使测试通过(绿),最后重构代码以优化...

    测试驱动开发With Junit

    在介绍JUnit前,我们需要理解TDD的核心思想,即“红-绿-重构”循环: 1. **红** - 创建一个测试用例,运行它,观察它失败(红色表示测试未通过)。 2. **绿** - 编写最小量的代码,使测试用例通过(绿色表示测试...

    pragmatic unit testing in java with junit

    《Pragmatic Unit Testing in Java with JUnit》是一本专注于Java单元测试的指南,它深入讲解了如何有效地利用JUnit框架进行测试驱动开发(TDD)。JUnit是Java领域中最广泛使用的单元测试框架,它为开发者提供了编写...

    class-3917:TDD w JUnit - iconectiv - 218

    标题 "class-3917:TDD w JUnit - iconectiv - 218" 暗示了这是一个关于测试驱动开发(TDD)的课程,使用的是JUnit框架,由iconectiv机构提供,可能是2018年2月的一次教学内容。描述 "带有 JUnit 的 TDD - iconectiv ...

    xUnit for Matlab with JUnit-compatible XML output.zip

    标题 "xUnit for Matlab with JUnit-compatible XML output.zip" 提供了一个关键信息,即这是一个针对Matlab的测试框架,它支持生成JUnit兼容的XML输出。这意味着你可以使用这个工具在Matlab环境中进行单元测试,...

    Testing Java with JUnit 5 & Mockito学习资料

    JUnit作为Java领域最常用的测试框架,已经发展到了第五版(JUnit 5),带来了许多新特性和改进。本学习资料主要围绕使用JUnit 5进行单元测试,并结合Mockito库进行模拟对象的测试。 首先,让我们了解一下如何将...

    Junit4.8.2(Junit4.7)

    JUnit是Java编程语言中最常用的单元测试框架之一,它由Ernst魔鬼和Kent Beck开发,最初是为了支持Extreme Programming(XP)实践中的测试驱动开发(TDD)。在本篇中,我们将详细探讨JUnit 4.8.2和4.7这两个版本,...

    junit4教程(《Junit4初探》)

    与JUnit3相比,JUnit4的灵活性和可扩展性得到了显著提升,使得测试驱动开发(TDD)在Java领域变得更加普及。 ## 二、JUnit4的核心组件 ### 1. 测试注解 - `@Test`: 表示一个方法是测试方法,可以包含断言。 - `@...

    JUnit4JUnit4JUnit4(文档)

    JUnit4 的这些特性使得编写和维护测试变得更加简单和高效,同时也鼓励了TDD(测试驱动开发)和持续集成的实践。无论是在小型项目还是大型企业级应用中,JUnit4 都是Java开发者不可或缺的测试工具。通过深入理解和...

    JUnit 4测试驱动开发----junit技术讲解

    理解并掌握JUnit 4的基本用法和高级特性,对于进行有效的单元测试和实践TDD至关重要。通过编写清晰、独立的单元测试,开发者可以确保代码的质量,降低bug出现的可能性,并促进软件的持续集成和持续交付。

    Junit实例_一个Junit的实例_

    在Java编程领域,JUnit是一个非常重要...总的来说,通过这个Junit实例,我们可以学习到如何使用JUnit进行单元测试,理解测试驱动开发(TDD)的重要性,以及如何编写和组织测试用例。这对于提高代码质量和维护性至关重要。

    junit4测试源码

    这个"junit4测试源码"可能包含了JUnit4框架的源代码,使得用户能够深入理解其内部工作原理,便于自定义扩展或学习测试驱动开发(TDD)的最佳实践。 首先,JUnit4引入了注解(Annotation)的概念,使得测试类和测试...

    对JUnit的一点简单讲解包括一点简单的Android JUnit的讲解

    AndroidJUnit4引入了`@RunWith(AndroidJUnit4.class)`,这使得我们可以使用JUnit4的特性,同时利用Android的测试环境。例如,`@UiThreadTest`注解用于表示该测试必须在主线程运行,而`@SmallTest`, `@MediumTest`, ...

    junit-4.4包+源码

    此外,还有`@Ignore`注解用于忽略某个测试,`@RunWith`用于指定运行器,比如`@RunWith(Parameterized.class)`可以实现参数化测试。 2. **断言改进**:JUnit 4.4提供了更多的断言方法,如`assertEquals`、`...

Global site tag (gtag.js) - Google Analytics