谈对JSF项目的单元测试

      不知道大家有没有对Web页面进行测试的经历?或者正想要对web页面进行测试?为什么我下面这篇文章没什么人回复呢?是因为JSF用到的人少,还是因为我比较超前没几个人想到对JSF页面进行自动测试呢?呵呵

      一直以来,我们使用java语言开发的程序主要是web应用程序而非桌面应用,当然java是完全可以用来开发桌面应用程序的,目前已经有了比较成熟的针对java的单元测试工具Junit,但是Junit在web应用开发过程中却很难派上大的用场,比如如何让程序对JSP页面进行自动测试?
      我们在太原铁路项目中普遍采用了JSF框架进行前端页面的展现和服务器端业务逻辑的处理,收到了比较好的效果,提高了工作效率,但是我们在程序开发过程中经常重复出现一些解决过的错误,为了解决这些错误又需要重新寻找出现错误的地方,或者因为改动了一个小的地方而导致其他地方出现问题我们却一无所知。最后我们只能通过一遍遍的点击页面重复测试才能重新树立对程序的信心,然而人是有厌倦心理的,对于一遍遍重复的低级劳动很快就会厌倦,尤其是程序员对自己所写的代码很厌倦测试,这样很难保证程序的质量。于是我们想过引入单元测试,重复的东西让计算机去做,然而对JSF页面进行单元测试是比较困难的,因为JSF程序都是运行在容器环境中的,与HTTP请求和相应是紧密关联,如果我们要对ManagedBean进行测试,需要构造上下文环境,这些上下文环境原本是由浏览器和JBoss之类的容器构造的,让我们自己去编写代码构造上下文环境很难也很不现实,对JSF页面的自动测试也是很难想象的,然而有了JSFUnit的参与这一切将变得很简单,JSFUnit就是我下面要介绍的针对JSF的测试工具。
      JSFUnit是jboss开源组织的一个开源项目,JSFUnit beta 1版于2007年11月发布,目前国内使用JSFUnit还比较少,相信日后一定会得到广泛的使用。JSFUnit被设计为通过使用简单的API来完成JSF项目的集成测试和单元测试的工具。它完全可以访问managed beans, FacesContext, EL表达式和JSF内部组件树,同时可以访问每次客户端请求后的HTML相应。
      当然JSFUnit还有其他一些功能,比如JSF静态分析测试可以测试JSF的配置文件faces-config.xml,帮你尽早发现配置问题。JSFTimer能够进行JSF生命周期每个阶段的性能测试。从此JSF的测试从难以想象到轻松变为现实。目前还只有JSFUnit的官方网站对它的使用方法进行了比较详细的介绍,如有兴趣可以登陆www.jsfunit.org或者http://www.jboss.com/jsfunit/进行查询。下面我将介绍一下我在使用JSFUnit中的一些经验,希望需要使用它的人能够少走弯路。


我先将官网的开始向导展示出来吧。
1) 首先,务必遵循JSFUnit的金科玉律,那就是使用组件的ID。
在一个页面中的每个组件,你可以选择是否为他们提供一个ID。如果你不愿意这么做的话,JSF会自动为你创建一个。然而,你让JSF为你创建的话,那么你在测试时将很难引用这些组件。因此,你至少应该为你想要测试的每个组件指定一个ID。
例如:
<h:inputText value="#{foo.text}" id="input_foo_text"/>
2) 创建一个JSP或Facelets页面
3)

<f:view> 
  <h:form id="form1"> 
     <h:outputText value="Enter your name:" rendered="#{empty foo.text}" id="prompt"/> 
     <h:outputText value="Hello #{foo.text}" rendered="#{!empty foo.text}" id="greeting"/> 
     <h:inputText value="#{foo.text}" id="input_foo_text"/> 
     <h:message for="input_foo_text" styleClass="errorMessage"/> 
   <br/> 
     <h:commandButton value="Submit" action="/index.jsp" id="submit_button"/> 
     <h:commandButton value="Goodbye" action="/finalgreeting.jsp" id="goodbye_button"/> 
   </h:form> 
</f:view> 



4) 使用JSFUnit Façade API创建一个JUnit 测试。

public class JSFUnitTest extends org.apache.cactus.ServletTestCase
{
   public static Test suite()
   {
      return new TestSuite( JSFUnitTest.class );
   }
   
   public void testInitialPage() throws IOException, SAXException
   {
      // Send an HTTP request for the initial page
      JSFClientSession client = new JSFClientSession("/index.faces");
      
      // A JSFServerSession gives you access to JSF state      
      JSFServerSession server = new JSFServerSession(client);

      // Test navigation to initial viewID
      assertEquals("/index.jsp", server.getCurrentViewID());

      // Assert that the prompt component is in the component tree and rendered
      UIComponent prompt = server.findComponent("greeting");
      assertTrue(prompt.isRendered());
  // Test a managed bean
      assertEquals("Stan", server.getManagedBeanValue("#{foo.text}"));
   }
}

 
5) 添加如下内容到你的web.xml文件中

<filter> 
<filter-name>JSFUnitFilter</filter-name> 
<filter-class>org.jboss.jsfunit.framework.JSFUnitFilter</filter-class> 
</filter> 

<filter-mapping> 
<filter-name>JSFUnitFilter</filter-name> 
<servlet-name>ServletTestRunner</servlet-name> 
</filter-mapping> 

<filter-mapping> 
<filter-name>JSFUnitFilter</filter-name> 
<servlet-name>ServletRedirector</servlet-name> 
</filter-mapping> 

<servlet> 
<servlet-name>ServletRedirector</servlet-name> 
<servlet-class>org.apache.cactus.server.ServletTestRedirector</servlet-class> 
</servlet> 

<servlet> 
<servlet-name>ServletTestRunner</servlet-name> 
<servlet-class>org.apache.cactus.server.runner.ServletTestRunner</servlet-class> 
</servlet> 

<servlet-mapping> 
<servlet-name>ServletRedirector</servlet-name> 
<url-pattern>/ServletRedirector</url-pattern> 
</servlet-mapping> 

<servlet-mapping> 
<servlet-name>ServletTestRunner</servlet-name> 
<url-pattern>/ServletTestRunner</url-pattern> 
</servlet-mapping> 

 
6) 将下面这些jars包放到你的WEB-INF/lib下面
      首先,你将需要JSFUnit.jar。因为JSFUnit使用JUnit,Cactus和HttpUnit 作为基础框架,你将至少需要这些框架的最小jar包。别担心,你不需要用这些独立的包弄脏你的工程。 JSFUnit文档现告诉了你在测试时怎样使用Ant或Maven来仅仅包含这些额外的JAR包。
下面是在你WEB-INF/lib路径下所需的最起码的几个jar包
• jboss-jsfunit-core-1.0-beta-1.jar
• aspectjrt-1.2.1.jar
• cactus-13-1.7.1.jar
• httpunit-1.6.1.jar
• junit-3.8.1.jar
• jtidy-4aug2000r7-dev.jar
7) 将cactus-report.xsl放在你的WAR文件的根路径下。
      点击此处获得cactus-report.xsl
8) 部署应用和运行测试
      部署你的JSF应用到所选择的应用服务器中。然后,在浏览器地址栏中输入如下URL就能运行测试程序了。
http://localhost:8088/web/ServletTestRunner?suite=com.foo.JSFUnitTest&xsl=cactus-report.xsl
其中ServletTestRunner类将会用JUnit运行测试并且在浏览器中显示结果。完成这些后,浏览器将会有如下输出。

9) 将JSFUnit测试作为你构建的一部分
      在你看到怎样通过浏览器运行JSFUnit测试之后,你可能想集成JSFUnit作为你工程的一部分或测试驱动开发过程的一部分。我们有针对Maven和Ant的文档从wiki page链接过来。
因为JSFUnit基础框架是基于Apache Cactus的,你能用任何Cactus支持的方式建立你的测试程序。
我们当前正在为Ant和Maven开发象Cactus一样的工具,他们的被用在JSFUnit上。我们有一个为第一个JSFUnit beta版的Ant任务为和在第二个JSFUnit beta版发布之前准备好一个Maven插件。
(开始向导展示完毕)
      最初,我完全按照官方网站上的使用说明,一步步将jsfunit测试程序添加到我的项目中去,然后启动jboss运行测试程序,没想到第一句就执行错误。这句如下:
// 为要测试的页面发送一个HTTP请求
JSFClientSession client = new JSFClientSession("/transformer_manage.faces");
运行后发生了空指针异常。我无从知道这是什么原因,因为对它还一点也不了解。后来我下载了一个简单的JSFUnit测试样例项目,单独运行后可以成功测试。很奇怪为什么在我的项目中却无法运行,是不是因为我的项目需要登陆才能访问页面,那怎样才能让测试代码自己登陆系统呢?这个问题肯定很多人都会遇到。随后经过进一步查找原因,发现当执行这一语句向服务器发送HTTP请求时,如果没有登陆(身份验证),一个已有的过滤器将会阻止这一访问并返回一个错误页面,这样当然就无法初始化这个要测试的页面了。那么怎么才能解决这一问题呢?后来通过访问官方网站很高兴找到了相关的信息,作者给出了如下的解决办法:
1、 构造自动登陆代码

21 private WebConversation login() throws IOException, SAXException{ 
  //创建一个与服务器的会话 
22   WebConversation wc = WebConversationFactory.makeWebConversation(); 
//请求访问登陆页面并获得响应,并把HTTP响应封装为一个对象。 
23   WebResponse resp= wc.getResponse( "http://localhost:8088/web/.../login.jsp" ); 
//查找响应中ID为“loginCheck”的form 
24   WebForm form = resp.getFormWithID("loginCheck"); 
25   form.setParameter("userName", "yd"); 
26   form.setParameter("passWord", ""); 
27   resp= form.submit(); 
28   return wc; 
29 } 

 
这一方法能够创建一个与服务器之间的会话(22行),然后创建一个登陆页面的相应对象(23行),设置用户名和密码,然后提交(27行),最后返回这个回话对象。
2、 用上一方法返回的回话对象创建客户端对象,一切成功!

34 public void testInialpage() throws IOException, SAXException { 
35   WebConversation wc = login(); 
       //将WC作为参数创建client对象 
36   client = new JSFClientSession( wc , /transformer_manage.faces"); 
37   // A JSFServerSession gives you access to JSF state 
38   server = new JSFServerSession(client); 
39   // Test navigation to initial viewID 
40   assertEquals(transformer_manage.jsp, server.getCurrentViewID()); 
41 } 

 40行是断言返回的页面的ID应该为transformer_manage.jsp,如果返回ID正确则测试通过。
JSFUnit是基于JUnit的,测试代码的规则和JUnit一致,凡是测试方法的名称都必须以test开头,JSFUnit在运行时会逐个执行这些测试方法。一般需要在这些测试方法开始执行前构造一些对象供测试方法调用,对象构造语句需写在serup()方法中,最后必须在tearDown()方法中将这些对象的引用置空,确保内存释放。
下面展示一下我工程中的一个测试类的例子。

public class TestParentTable extends ServletTestCase { 
  private JSFClientSession client; 
  private JSFServerSession server; 

  public void setUp() throws IOException, SAXException 
  { 
     testInialpage(); 
  } 

  public static Test suite() { 
      TestSuite ts=new TestSuite(TestParentTable.class,"单元测试"); 
      return ts; 
  } 

   private WebConversation login() throws IOException, SAXException{ 
     …… 
   } 

   public void testInialpage() throws IOException, SAXException { 
      login(); 
      …… 
   } 
  public void testAdd() throws IOException, SAXException { 
  //初始时,断言transformer对象的currentObject属性为NULL值 
  assertNull(server.getManagedBeanValue("#{transformer.currentObject}")); 
  //模拟点击页面中“增加”按钮这一事件 
  client.clickCommandLink("initAdd"); 
  //断言页面ID应该为updatePage+".jsp",以判断导航是否正确 
   assertEquals(updatePage+".jsp", server.getCurrentViewID()); 
  //断言transformer对象的currentObject属性为非NULL值 
  assertNotNull(server.getManagedBeanValue("#{transformer.currentObject}")); 
  } 

  public void testAddSave() throws IOException, SAXException { 
  …… 
  } 
  public void testUpdate() throws IOException, SAXException { 
  …… 
  } 
  public void testDelete() throws IOException, SAXException { 
  …… 
  } 
  public void testQuery() throws IOException, SAXException { 
  …… 
  } 

    protected void tearDown() throws Exception { 
      client=null; 
      server=null; 
    } 
} 

 
运行测试程序后将会以网页的形式展示测试的结果: 【请看附件1】

      这里仅仅是给出了一个简单的展示,省略了一些细节,我将会写出更加详细的使用文档。总之,JSFUnit能够完全模拟人点击页面上各按钮或链接等各种操作,然后获得操作结果,以测试程序是否正确。
      我们总是在书上或网络上看见对单元测试必要性的宣传,我这里并不想人云亦云,极力鼓吹单元测试的好处,因为我并没有亲身去体验过一个项目完整的写下了单元测试用例所带来的好处。我只是希望在这方面去进行尝试。毕竟用代码去编写单元测试或集成测试是需要花费时间的,本来项目已经很紧了,程序员很可能不愿意去花费另外的时间编写测试代码。但是如果我们只需要花不多的时间就能完成这些代码,并且这些代码能够为我们以后的测试节省更多的时间那样会不会更划算呢?如果利用JSFUnit能够很好的完成这一点的话,我相信编写测试代码是值得的。
      当然我们需要对测试粒度的大小有一个比较折中的选择,粒度太大可能得不到比较好的测试效果,粒度太小会导致花费在测试代码的时间比程序本身还要多。我们写测试代码不可能把所有逻辑路径全覆盖一遍,这样做将花费太多的时间,不得不进行一些舍弃,从而也会带来一些问题,导致我们对程序质量过于自信,虽然测试代码全部通过了,但是任然存在一些未知的错误。我已经进行了一些尝试,认为JSF项目针对JSF页面而非直接对ManagedBean进行测试应该比较好,对ManagedBean测试可能需要构造一些环境参数比如ActionEvent,这是比较困难的。如果我们对JSF页面如上面的代码所示可以模仿真实的人工测试的操作序列,这似乎已不属于单元测试了,而是集成测试,但这种测试粒度应该比较合适。当然这不一定就是最好的办法,在这个问题上需要继续讨论,继续进行新的探索。
      想一想以后点击一下“测试”按钮,测试程序将把所有页面从头至尾自动测试一遍,将是多么惬意的事情,感谢JSFUnit。大家一起讨论讨论还有什么好的JSF测试工具!

猜你喜欢

转载自peter196.iteye.com/blog/189257