UML软件工程组织

使用JUnit进行容器内测试
作者:Julien D…

了解使用 JUnit 进行容器内测试如何优于模拟对象进行集成测试,以及如何使用 Oracle JDeveloper 来应用该技术。

单元测试作为一种确保代码质量的技术现如今非常流行。由于有了 JUnit 框架,因此为简单的 Java 应用程序编写单元测试就变得容易多。然而,对于真实世界的企业应用程序来说,因为这些应用程序需要对象在容器内运行,所以常用的 JUnit testXXX() 方法不是很管用。

在本文中,为了让 JUnit 测试访问在 J2EE 容器内运行的对象,我将介绍容器内测试的应用。此处使用的示例应用程序是一个基于 Struts 的 Web 应用程序,这种应用程序在企业应用程序中相当普遍,但是所讨论的技术却与所有 J2EE 项目都相关。

JUnit 介绍

了解 JUnit 的最佳方式是看一些代码,因此我们先来看一个名为“Message”的 JavaBean 简单示例。

package jdubois.otn.cactus.domain;

import java.io.Serializable;
import java.util.Calendar;
import java.util.Date;

public class Message implements Serializable, Comparable {

private Date postDate;
private String user;
private String comment;

public Message(String user) {
this.postDate = Calendar.getInstance().getTime();
this.user = user;
  }

public void setUser(String user) {
this.user = user;
  }

public String getUser() {
return user;
  }

public void setComment(String comment) {    
this.comment = comment;
  }

public String getComment() {
return comment;
  }

public Date getPostDate() {
return postDate;
  }
  
public int compareTo(Object o) {
return this.postDate.compareTo(((Message) o).getPostDate());
  }
}
下面是一个简单的 JUnit 测试,该测试检查该 JavaBean 构造器:
package jdubois.otn.cactus.test;

import junit.framework.*;
import jdubois.otn.cactus.domain.Message;

public class MessageTester extends TestCase {
public MessageTester(String sTestName) {
super(sTestName);
  }

public void testMessage() {
Message message = new Message("user");
assertEquals("user", message.getUser());
assertNotNull(message.getPostDate());
  }
  
public static void main(String[] args) {
junit.textui.TestRunner.run(MessageTester.class);
  }
}
对于 JUnit 要作的操作是创建一个 testXXX() 方法,在该方法内运行一些代码,然后检查对于各种 assertXXX(..) 方法所有代码是否都运行正常。要编写这样的代码相当简单,但是对于测试如今的 J2EE 应用程序来说真正的用处并不大。实际上,当您涉足真正的企业应用程序领域时,事情会变得复杂得多;主要的问题在于,在 J2EE 领域,大多数对象都需要在容器内运行。

我们来编写一个简单的 Struts 操作:

package jdubois.otn.cactus.web;

import java.util.TreeSet;
import org.apache.struts.action.*;
import javax.servlet.http.*;

public class LoginAction extends Action {

public ActionForward execute(ActionMapping mapping, ActionForm form, 
HttpServletRequest request, HttpServletResponse response) 
throws java.io.IOException, javax.servlet.ServletException {
    
LoginForm frm = (LoginForm) form;
request.getSession().setAttribute("user", frm.getName());
    
if (request.getSession().getServletContext().getAttribute("messages") == null) {
request.getSession().getServletContext().setAttribute("messages", new TreeSet());
    }
return mapping.findForward("success");
  }
}
execute(...) 方法确实难以测试。创建 ActionForm 过程正常;但是怎样才能在容器外生成 ActionMapping、HttpServletRequest 和 HttpServletResponse 呢?J2EE 中的大多数对象都可能发生同样的问题;这些对象本来就不是为在容器外运行设计的,因此很难使用没有容器的普通 JUnit 测试对这些对象进行测试。

处理此问题的方法常用的有两种:模拟对象和容器内测试。

模拟对象模拟真实世界中的 J2EE 对象。为了在一个没有容器运行的 JUnit 测试中使用这些对象,需要一个框架(如 MockObjects,http://www.mockobjects.com)提供某些 J2EE API 的特殊实现。例如,它提供“MockHttpServletRequest”对象。

容器内测试使 JUnit 测试可以在 J2EE 容器内运行。为了实现此目标,根据所用工具的不同(在本文中为 Cactus 和 HttpUnit),有时需要稍微修改要测试的 J2EE 应用程序。

本文将重点讨论第二中方法,因为使用此方法可以轻松地建立和执行“现实生活”测试。容器内测试的代码编写工作较少,并且会增加一层在实践中非常有用的集成测试。从本作者的观点来看,当前的模拟对象框架还有待成熟且文档欠缺。容器内测试的缺点则在于,由于需要在 J2EE 服务器上部署应用程序,因此该种测试运行速度较慢;不过因为 OC4J 的启动速度相当快,所以对它来说应该不是什么问题。

概述

示例应用程序。就本文来讲,将创建一个名为“Discussion Board”的示例应用程序。这是一个非常简单的讨论公告牌,用户可以在此进行登录和张贴信息。

为了简单起见,此示例不使用持久性存储;换句话说,就是不使用数据库。真实世界中不存在这种简单情形。大多数项目都使用对象关系映射工具,如 CMP EJB 或 TopLink,因此该测试不必关注持久层。

使用的工具。 运行容器内测试需要一些开放源代码项目的帮助。

  • Cactus (http://jakarta.apache.org/cactus/index.html) 是 JUnit 的扩展,它在很大程度上简化了运行容器内测试的过程。Cactus 提供一个用于测试 JSP、Servlet、taglib 和 servlet 筛选器的框架,并具有详尽的文档。
  • (http://strutstestcase.sourceforge.net) 是 Cactus 的扩展,专为测试 Struts 代码开发。简单来讲,StrutsTestCase 是 Cactus 的扩展,而 Cactus 又是 JUnit 的扩展。通过结合使用这三个项目,即可对大范围的对象(从简单的 JavaBean,到 Servlet,再到 Struts 操作)轻松执行单元测试。
  • HttpUnit (http://httpunit.sourceforge.net) 采用的方式有点不同。它模拟 Web 浏览器。当与 JUnit 一起使用时,允许对应用程序生成的 Web 页进行高效测试。因此,它更大程度上是一个“黑盒”测试工具。
安装 JUnit、Cactus、StrutsTestCase 和 HttpUnit

JUnit。安装 JUnit 非常简单,只需启动 JDeveloper,单击“help -> check for updates”,然后从可用扩展中选择 JUnit 即可。为了与 Ant 一起使用 JUnit,请将 junit.jar 文件从 <JDEVELOPER_INSTALL_DIR>\junit3.8.1 复制到 ant 的“/lib”目录中。

Cactus。 Cactus 是一个 Apache Jakarta 项目,可在 http://jakarta.apache.org/cactus/index.html 处找到。下载页面位于 http://jakarta.apache.org/cactus/downloads.html。对于本文,使用的是“jakarta-cactus-13-1.6.1.zip”文件。将此文件解压缩到一个目录中,例如 c:\java\cactus。

StrutsTestCase。 StrutsTestCase 是一个 SourceForge 项目,可在 http://strutstestcase.sourceforge.net 处找到。对于本文,使用的是“strutstest210-1.1_2.3.zip”文件。将此文件解压缩到一个目录中,例如 c:\java\strutstest。

HttpUnit。HttpUnit 也是一个 SourceForge 项目,可在 http://httpunit.sourceforge.net 处找到。对于本文,使用的是 1.5.4 版。将“httpunit-1.5.4.zip”解压缩到一个与 StrutsTestCase 相邻的目录中,例如 c:\java\httpunit。

配置 JDeveloper

  • 启动 JDeveloper,创建一个新的应用程序工作区。
  • 右键单击该项目,选择“project properties”,转至“profiles -> development -> libraries”。
  • 添加 Cactus 库:
    • 选择“new”,将这个新库命名为“Cactus”。
    • 在类路径中,添加 <CACTUS_INSTALL_DIR>\lib 中包含的、除 junit-3.8.1.jar 和 servletapi-2.3.jar(这两个库稍后再添加到类路径中)之外的所有库。
    • 还可以添加 Cactus 文档;该文档存储在 <CACTUS_INSTALL_DIR>\doc\api\framework-13 中。
  • 添加 StrutsTestCase 库:
    • 选择“new”,将这个新库命名为“StrutsTest”。
    • 在类路径中,添加“<STRUTSTESTCASE_INSTALL_DIR>\strutstest-2.1.0.jar”文件。
    • 还可以安装文档;文档保存在“<STRUTSTESTCASE_INSTALL_DIR>\docs\api”中。
  • 添加 HttpUnit 库:
    • 选择“new”,将这个新库命名为“HttpUnit”。
    • 在类路径中,添加“<HTTPUNIT_INSTALL_DIR>\jars”目录中的 nekohtml.jar 和 xercesImpl.jar。这是开始使用 HttpUnit 的最简单方式。如果这种方式不适合您,则可以使用 JTidy(也位于 /jars 目录中),这是一个更加严格的 HTML 分析器。
    • 如果您需要 JavaScript 支持,则添加 js.jar 库(位于相同的目录中)即可。这个库运行性能非常好 - 至少对于变化格式的小脚本来说如此 - 因此,即使在客户端执行一些神奇的 JavaScript 也可以测试 Web 站点。
    • 添加 HttpUnit 库,该库位于“<HTTPUNIT_INSTALL_DIR>\lib”中。
    • 文档位于“<HTTPUNIT_INSTALL_DIR>\doc\api”。

重要注意事项:Cactus 和 HttpUnit 一起运行时性能较差,因为它们都使用 NekoHtml 库。因为它们包含了相同的功能,所以希望您不会同时使用它们两个。但是,如果您希望在 JDeveloper 中同时使用这二者,则必须将 NekoHtml 和 HttpUnit(而不是 JTidy)一起使用,而且必须构建一个包含所有 Cactus 和 HttpUnit jar 的 JDeveloper 库。如果您遇到了 java.lang.reflect.InvocationTargetException,则是由于上述问题产生的。该项目所需的其他库,如 JUnit 运行时、JSP 运行时、Struts 运行时和 Servlet 运行时,则由 JDeveloper 在创建示例应用程序期间自动添加。

创建示例应用程序

该示例应用程序是一个基于 Struts 的讨论公告牌。在 JDeveloper 中创建一个新应用程序,然后右键单击该项目:选择 new -> Web tier -> Struts -> Struts Controller Page Flow。

将使用图 1 中的页面流:

图 1
图 1:基于 Struts 的讨论公告牌的页面流示例

下面是 struts-config.xml 文件的源代码:

<?xml version = '1.0' encoding = 'windows-1252'?>
<!DOCTYPE struts-config 
PUBLIC "-//Apache Software Foundation//DTD Struts Configuration 1.1//EN" 
"http://jakarta.apache.org/struts/dtds/struts-config_1_1.dtd">
<struts-config>
<form-beans>
<form-bean name="loginForm" type="jdubois.otn.cactus.web.LoginForm"/>
<form-bean name="messageForm" type="jdubois.otn.cactus.web.MessageForm"/>
</form-beans>
<action-mappings>
<action name="loginForm" path="/Login" 
type="jdubois.otn.cactus.web.LoginAction" unknown="true" input="/login.jsp">
<forward name="success" path="/messages.jsp" redirect="true"/>
</action>
<action name="messageForm" path="/AddMessage" 
type="jdubois.otn.cactus.web.AddMessageAction" unknown="false">
<forward name="success" path="/messages.jsp"/>
<forward name="login" path="/login.jsp"/>
</action>
</action-mappings>
<message-resources parameter="jdubois.otn.cactus.web.ApplicationResources"/>
</struts-config>
为了生成页面流图,将上面的代码复制到 struts-config.xml 文件中,转至“diagram”选项卡,然后刷新(右键单击 -> diagram -> refresh diagram from struts-config)。

从一个简单的 JUnit 测试开始

  1. 首先,按照介绍部分的说明创建 Message JavaBean。
  2. 右键单击“Application Sources”,添加一个新类。复制/粘贴介绍部分中的代码。
  3. 现在,我们接着创建介绍部分中的简单 JUnit 测试:右键单击“Application Sources”-> General -> Unit Tests (JUnit) -> Test Case。
  4. 在向导中,将新创建的 Message JavaBean 选中,作为要测试的类。
  5. 生成的文件即可用来进行测试了;只是它还需要一些 testXXX() 方法,如介绍部分中说明的那个方法。复制并粘贴 testMessage() 方法:
    public void testMessage() {
    Message message = new Message("user");
    assertEquals("user", message.getUser());
    assertNotNull(message.getPostDate());
      }
    
  6. 为了简化在 JDeveloper 中的开发,可以添加一个 main(...) 方法:
    public static void main(String[] args) {
    junit.textui.TestRunner.run(MessageTester.class);
      }
    
    添加此方法后,JDeveloper 会识别该 JUnit 测试,右键单击该类时会出现一个“Run -> As a JUnit Test Case”菜单。
  7. 使用此菜单,可在位于屏幕底部的 JDeveloper 控制台中执行该 JUnit 测试。
当然,我们感兴趣的是测试失败的情形,而不是测试成功的情形。我们在该方法中添加下列代码,使得上面的测试失败:
assertTrue(false);

此代码总会导致失败,而 JUnit 将识别该失败。发生这种问题时(我们本来就希望发生这种问题 — 毕竟,我们在这创建测试就是为了获得这种效果),有两个有用的工具可以使用:

  • JUnit 提供良好的堆栈跟踪,在上面可以进行点击,以便找出代码失败的位置。
  • JDeveloper 调试器能够与 JUnit 一起运行,因此可以在打开调试器并设置一些断点的情况下再次运行该测试。
创建测试套件

测试套件由一组一组的测试构成。在 JDeveloper 中,测试套件尤为方便:

  • 可以使用向导以图形化的形式创建测试套件。
  • 测试套件会启动 JUnit 图形界面。
以下说明如何创建一个简单的测试套件:
  1. 右键单击“Application Sources”,单击“Unit Tests”组中的“Test Suite”,新建一个测试套件。
  2. 当向导提示时,将您的测试套件命名为“AllTests”,并将先前创建的测试用例放入该测试套件中。
  3. 完成向导后,运行该测试套件(参见图 2):

图 2
图 2:运行测试套件

快速浏览一下生成的测试套件源代码就会发现,向测试套件中添加新测试用例非常简单:

suite.addTestSuite(jdubois.otn.cactus.test.MessageTester.class);

使用 Cactus 测试示例应用程序

既然已经搞清楚了一个简单的 JUnit 测试,现在该转到本文的主题了:J2EE 应用程序测试。

运行任何 Cactus 容器内 J2EE 测试之前,先要将应用程序“Cactus 化”。只要修改 web.xml 并添加一个 properties 文件即可。这种“Cactus 化”可以手动完成,也可以通过使用 Ant 脚本来完成。

手动 Cactus 化。为了从标准的 JUnit Testrunner 类中对容器运行单元测试,Cactus 使用了一个特殊的 servlet。

这就需要向 web.xml 文件中添加一个新的 servlet 及其关联的映射:

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

Cactus Testrunner 类需要在类路径中有一个名为 cactus.properties 的 properties 文件。右键单击“Application Sources”树,添加一个新文件,并在这个源树的根部创建“cactus.properties”。此文件需要三个属性:

cactus.contextURL = http://localhost:8988/user-Project-context-root
cactus.servletRedirectorName=ServletRedirector
cactus.enableLogging=true

“user-Project-context-root”是当前应用程序的部署环境。如果您对此环境的名称不是很确定,则右键单击“login.jsp”文件,然后选择“run”:在新打开的浏览器中,将看到指向当前应用程序的 URL。

<war/> 和 <cactifywar/> Ant 任务。<war/> 任务是一个用于创建 war 的标准 Ant 任务。

<!-- create the war file -->
<war destfile="./discussion-board.war" webxml="public_html/WEB-INF/web.xml">
<fileset dir="public_html">
<exclude name="WEB-INF/web.xml"/>
</fileset>
<lib dir="${jdev.home}/jakarta-struts/lib/*">
</lib>
<classes dir="classes"/>
</war>

在 Ant 网站上还可以找到更多的文档和示例:http://ant.apache.org。

Cactus 发行套件为 Ant 提供了一些其他任务。最重要的一个任务是 <cactifywar/> 任务。

<cactifywar/> 任务分析常见的 war 文件,并将其转换为 Cactus 化的 war 文件。它可以自动完成我们在前面部分执行的手动步骤。唯一的一个小缺点是运行速度稍微有些慢,因为它要不停地解压缩/Cactus 化/压缩 war 文件。它还会一直保存一个标准的、非 Cactus 化的 war 文件用于部署。要注意的重要一点是,不应将 ServletRedirector servlet 绑定在生产 war 中,因为这样可能会成为一个潜在的安全漏洞。

从 Cactus 网站 (http://jakarta.apache.org/cactus/integration/ant/index.html) 可知,Cactus 与 Ant 的集成相当简单:

<!-- Define the Cactus tasks -->
<!-- If you generate the build.xml file with JDeveloper, all the Cactus files should 
already be in the classpath, so there is no need to redefine it -->
<taskdef resource="cactus.tasks">
<classpath refid="classpath"/>
</taskdef>

<!-- Cactify the web-app archive -->
<cactifywar srcfile="./discussion-board.war"
		destfile="="./cactified-discussion-board.war">
      			
<servletredirector/>
<lib file="<STRUTSTESTCASE_INSTALL_DIR>/strutstest-2.1.0.jar"/>
</cactifywar>

如果您使用 Ant 和 Cactus 时出现问题,首先要做的就是按照本文“安装 JUnit”部分的说明,将 junit.jar 文件复制到 ant 的“/lib”目录中。Cactus 提供了一些其他 Ant 任务来自动化测试流程;本文最后的“接下来的步骤”部分将对这些任务进行了说明。

创建和测试 LoginAction

创建该操作。LoginAction 使用一个 FormBean 和两个 JSP(login.jsp 和 messages.jsp)。我们来创建这三个对象。

创建这些文件的一个简单方式是打开 Struts 页面流:

  • 双击 LoginAction 创建 LoginAction。
  • 右键单击该操作和“go to FormBean”创建 FormBean。
  • 双击 JSP 创建 JSP。

LoginAction.java

package jdubois.otn.cactus.web;

import java.util.TreeSet;
import javax.servlet.http.*;
import javax.servlet.ServletContext;

import org.apache.struts.action.*;

public class LoginAction extends Action {

public ActionForward execute(ActionMapping mapping, ActionForm form, 
HttpServletRequest request, HttpServletResponse response) 
throws java.io.IOException, javax.servlet.ServletException {
    
LoginForm frm = (LoginForm) form;
request.getSession().setAttribute("user", frm.getName());
ServletContext context = request.getSession().getServletContext();
    
if (context.getAttribute("messages") == null) {
context.setAttribute("messages", new TreeSet());
    }
return mapping.findForward("success");
  }
}

LoginForm.java

package jdubois.otn.cactus.web;

import org.apache.struts.action.*;
import javax.servlet.http.HttpServletRequest;

public class LoginForm extends ActionForm {

private String name;

public ActionErrors validate(ActionMapping mapping, HttpServletRequest request) {
ActionErrors errors = new ActionErrors();
if (this.name == null || this.name.equals("")) {
errors.add("error", new ActionError("user.name.required"));
    }
return errors;
  }

public void setName(String name) {
this.name = name;
  }

public String getName() {
return name;
  }
}

请注意,ActionError 使用 Struts 的 ApplicationResources.properties 文件中指定的“user.name.required”键:

user.name.required=A user name is required

login.jsp

<%@ taglib uri="/WEB-INF/struts-html.tld" prefix="html"%>
<%@ page contentType="text/html;charset=windows-1252"%>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=windows-1252">
<title>Welcome</title>
</head>
<body>
<div align="center">
<html:form action="/Login">
<h2>Welcome to our online discussion board</h2>
<html:errors/>
<p>Please enter your name : 
<html:text property="name" size="20"/>
<html:submit value="Login"/>
</p>
</html:form>
</div>
</body>
</html>

messages.jsp

<%@ taglib uri="/WEB-INF/struts-html.tld" prefix="html"%>
<%@ taglib uri="/WEB-INF/struts-bean.tld" prefix="bean"%>
<%@ taglib uri="/WEB-INF/struts-logic.tld" prefix="logic"%>
<%@ page contentType="text/html;charset=windows-1252"%>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=windows-1252">
<title>Discussion</title>
</head>
<body>
<div align="center">
<table border="1">
<tr>
<th>User</th>
<th>Message</th>
</tr>
<logic:iterate id="message" name="messages">
<tr>
<td>
<bean:write name="message" property="user"/>
</td>
<td>
<bean:write name="message" property="comment"/>
</td>
</tr>
</logic:iterate>
</table>
</div>
</body>
</html>

右键单击“login.jsp”,选择“run”。将打开一个浏览器窗口,现在就可以使用该 LoginAction(参见图 3)了:

图 3
图 3:讨论公告牌登录窗口

使用 StrutsTestCase 测试该操作。 开始测试时,请首先问您自己这样一个问题“我们要测试什么”。

  • 一个成功的登录会在会话范围内创建一个“user”对象
  • 一个成功的登录会将该用户转向 messages.jsp 页面
  • 一个失败的登录不会在会话范围内创建“user”对象
  • 一个失败的登录会将该用户重新指向登录页面
  • 一个失败的登录会生成一个“user.name.required”ActionError

由该应用程序将来的用户来回答这些问题将有助于定义应用程序流。

现在我们来测试该 LoginAction。选择“Application Sources”,创建一个新类。

LoginActionTester.java

package jdubois.otn.cactus.test;

import servletunit.struts.CactusStrutsTestCase;

public class LoginActionTester extends CactusStrutsTestCase {
  
public LoginActionTester(String testName) {
super(testName);
  }

public void testSuccessfulLogin() {
addRequestParameter("name","Julien");
setRequestPathInfo("/Login");
actionPerform();
verifyForward("success");
verifyForwardPath("/messages.jsp");
assertEquals("Julien", getSession().getAttribute("user"));
verifyNoActionErrors();
  }

public void testFailedLogin() {
setRequestPathInfo("/Login");
actionPerform();
verifyInputForward();
verifyForwardPath("/login.jsp");
verifyActionErrors(new String[] {"user.name.required"});
assertNull(getSession().getAttribute("user"));
  }

public static void main(String[] args) {
junit.textui.TestRunner.run(LoginActionTester.class);
  }
}

此测试用例为 CactusStrutsTestCase,它是一个由 StrutsTestCase(先前安装的)提供的对象。它同时使用 JUnit 和 Cactus 来测试 Struts 操作。

在这里,testSuccessfulLogin() 方法会:

  • 执行该操作
  • 确认该操作转向“成功”,“成功”实际上就是“messages.jsp”页面
  • 确认创建了“user”对象
  • 确认没有产生任何 ActionError

testFailedLogin() 方法则会:

  • 执行该操作
  • 确认该操作转向输入页面(即“login.jsp”)
  • 确认声明了“user.name.required”ActionError
  • 确认没创建“user”对象

要运行此测试:

  • 右键单击“login.jsp”,并运行该 jsp。这将在 OC4J 中更新这个 Web 应用程序。
  • 右键单击 LoginActionTester,单击“as a JUnit Test Case”来运行它。

最终的结果将会与前面的简单 JUnit 测试示例一样显示在 JDeveloper 控制台中。

我们现在将该测试用例添加到先前创建的 AllTests 测试套件中:

suite.addTestSuite(jdubois.otn.cactus.test.LoginActionTester.class);

重新启动 OC4J,并运行“AllTest”。图 4 就是最终的结果:

图 4
图 4:测试您的代码

此示例混合使用了一些 JUnit 方法(如 assertTrue())和一些 StrutsTestCase 方法(如 verifyForward())。因为 StrutsTestCase 是 JUnit 的扩展,所以适用于 JUnit 的也同样适用于 J2EE 单元测试。

用于测试 LoginAction 的方法非常简单,但是我们开始时使用这些方法就足够了。这些方法提供的功能足以确认 Struts 操作执行正确。

使用 HttpUnit 测试生成的 HTML

现在我们来再测试一个成功登录,这次我们使用 HttpUnit。正如本文安装部分所述,HttpUnit 与 Cactus 一起运行时性能不是很好。编写此测试的最佳方式是从您的 Oracle JDeveloper 项目临时删除 Cactus 库,以避免任何类加载错误。

HttpUnit 本身不是 JUnit 扩展,但 Cactus 是 JUnit 扩展。在某种意义上,它的名称会引起误解,因为它所提供的是一组充当浏览器的类:这些类处理表单、表格、甚至 JavaScript 和 cookie。但是这些类与 JUnit 一起使用时,会变成一个功能强大的黑盒测试机制,这点在下面的示例中可以看到;在下面的示例中,将在一个标准的 JUnit 测试用例中使用 HttpUnit:

package jdubois.otn.cactus.test;

import junit.framework.*;
import com.meterware.httpunit.*;

public class LoginActionHttpTester extends TestCase {
public LoginActionHttpTester(String sTestName) {
super(sTestName);
  }

public void testSuccessfulLogin() throws Exception {
WebConversation wc = new WebConversation();
WebRequest req = 
new GetMethodWebRequest("http://127.0.0.1:8988/your-project-context-root/login.jsp");
WebResponse resp = wc.getResponse(req);
assertTrue(resp.getText().indexOf("Welcome to our online discussion board") > 0);
WebForm form = resp.getFormWithName("loginForm");
form.setParameter("name", "Julien");
form.submit(); 
resp = wc.getCurrentPage();
assertEquals("Discussion", resp.getTitle());
  }
  
public static void main(String[] args) {
junit.textui.TestRunner.run(MessageTester.class);
  }
}

此代码是一个标准的 JUnit 测试用例,可以作为普通测试用例添加到测试套件中。当然,只有将 Web 应用程序正确部署到应用服务器上时,HttpUnit 才能起作用。实现此目的的最佳方式是,先运行一个应用服务器实例,并在运行时重新热部署被测试的应用程序,然后再运行该 JUnit 测试。对于 JBoss,上述方式意味着将生成的 war/ear 复制到 /deploy 目录,并检查 login.jsp 页面是否可用。有人可能会提出置疑,一直重新热部署可能会导致应用服务器的严重破坏,但是以本作者的经验,您可以执行数百次这样的操作也不会损坏应用服务器 - 如今的热部署技术已经足够成熟,完全可以应对类似事件。

我们稍后将看到,Cactus 不存在这种问题,它针对该问题提供了自己的 Ant 任务。将来,Cactus 的创建者 Vincent Massol 可能会发布一个用于启动/停止应用服务器的通用框架,该框架将使 HttpUnit 的使用更加方便。

Cactus 和 HttpUnit 的取舍。正如我们已经看到的那样,Cactus 和 HttpUnit 能够提供相同种类的测试。HttpUnit 更大程度上是一个黑盒测试工具。它是一个语言不可知测试工具(例如,您可以使用它来测试 PHP 网页)。而且,它对于测试者来说比较容易理解和使用。但是,如果您想要使用 J2EE 技术,尤其是 Struts 操作,Cactus 则是一个更好的工具。例如,StrutsTestCase 对于 ActionErrors 的处理非常有效。由于测试不依赖于 HTML 表示层,因此它所需要的维护也少得多。

对 AddMessageAction 使用极端编程

LoginAction 采用了非常传统的开发方式:首先开发人员编写应用程序代码,然后测试人员编写 JUnit 测试。如今,有了一种不同的方式:极端编程 (XP) 和测试先行开发。

在 XP 中,开发人员会在编写任何其他代码之前首先编写测试。这是一种非常现代和流行的技术,并且这种技术完全以客户为中心。XP 入门的最佳方式可能就是阅读 Kent Beck 非常优秀的书籍“Extreme Programming Explained”,但就这个主题已经发表了很多其他书籍,并且 XP 届在 Internet 上也提供了很多文档。

那么,我们就从一个普通的问题开始吧:我们要做什么?

  • 创建一个会添加新消息的操作。
  • 如果用户没有登录,则将该用户转向登录页面。
  • 如果该用户没有编写注释,则只是刷新当前页面。

下面是该 JUnit 测试用例的代码:

AddMessageActionTester.java

package jdubois.otn.cactus.test;

import java.util.Collection;
import servletunit.struts.CactusStrutsTestCase;

public class AddMessageActionTester extends CactusStrutsTestCase {
  
public AddMessageActionTester(String testName) {
super(testName);
  }
  
public void testAddMessage() {
request.getSession().setAttribute("user", "Julien");
addRequestParameter("comment","First post!");
setRequestPathInfo("/AddMessage");
actionPerform();
verifyForward("success");
verifyForwardPath("/messages.jsp");
assertEquals("Julien", getSession().getAttribute("user"));
    
Object messages = request.getSession()
.getServletContext().getAttribute("messages");
      
assertNotNull(messages);
if(!(messages instanceof Collection)) {
fail("Messages should be a Collection.");
    }
verifyNoActionErrors();
  }
  
public void testAddMessageNoUser() {
addRequestParameter("comment","First post!");
setRequestPathInfo("/AddMessage");
actionPerform();
verifyForward("login");
verifyForwardPath("/login.jsp");
verifyNoActionErrors();
  }
  
public static void main(String[] args) {
junit.textui.TestRunner.run(AddMessageActionTester.class);
  }
}

将该测试用例加入“AllTests”测试套件中,重新启动 OC4J,然后启动该测试套件。

整个应用程序会进行编译和部署;不过,正如所预料的,这个新测试会失败。因此,甚至在编写一个操作之前也可以创建测试用例。

这种技术可以用于大规模的测试;这样,可以将 JUnit 用作一个轻型项目管理工具。JUnit 可以提供报告,指示项目的状态。例如,就我们的示例而言,五个测试中有两个失败,因此我们项目的完成率大约为 60%。

现在我们来完成我们的工作,编写 AddMessageAction 代码:

package jdubois.otn.cactus.web;

import java.util.Collection;
import javax.servlet.http.*;
import org.apache.struts.action.*;

import jdubois.otn.cactus.domain.Message;

public class AddMessageAction extends Action {

public ActionForward execute(ActionMapping mapping, ActionForm form, 
HttpServletRequest request, HttpServletResponse response) 
throws java.io.IOException, javax.servlet.ServletException {

if (request.getSession().getAttribute("user") == null ||
request.getSession().getServletContext().getAttribute("messages") == null) {
return mapping.findForward("login");
    }
    
String user = (String) request.getSession().getAttribute("user");
MessageForm frm = (MessageForm) form;
if (frm.getComment() != null && !frm.getComment().equals("")) {
Message message = new Message(user);
message.setComment(frm.getComment());
      
Collection messages = (Collection) request.getSession()
.getServletContext().getAttribute("messages");
      
messages.add(message);
    }
frm.setComment("");
return mapping.findForward("success");
  }
}

MessageForm:

package jdubois.otn.cactus.web;

import org.apache.struts.action.*;
import javax.servlet.http.HttpServletRequest;

public class MessageForm extends ActionForm {

private String user;
private String comment;

public void setUser(String user) {
this.user = user;
  }

public String getUser() {
return user;
  }

public void setComment(String comment) {
this.comment = comment;
  }

public String getComment() {
return comment;
  }
}

由于需要从 JSP 页面调用该操作,因此必须将该操作添加到 messages.jsp 中:

<html:form action="/AddMessage">
<P>User : 
<bean:write name="user"/>
</P>
<P>
<html:textarea property="comment"/>
</P>
<P>
<html:submit value="Add a new message"/>
</P>
</html:form>

现在,右键单击 login.jsp,选择“run”。该应用程序应该能够圆满运行,您可以邀请一些开发人员在这个讨论公告牌上与您交流(参见图 5)。

图 5
图 5:讨论公告牌应用程序快照

恭喜!由于所有五个 JUnit 测试均已成功,因此现在整个测试套件应该已经成功了。记住这句口头禅:“Keep the bar green to keep the code clean”(使代码没有错误,从而保持代码精炼)。

以下是一些提供有关此主题更多信息的资料来源,都非常不错:

  • 阅读其他项目文档。
  • 订阅项目“用户”邮件列表,这些邮件列表非常活跃。
  • 购买一些专业书籍,例如,Vincent Massol 的优秀书籍“JUnit in Action”。

接下来的步骤

有关 Struts 的问题以及测试 EJB。Struts 操作只应用作表示层和业务层之间的粘合剂。一个常见的错误就是将业务逻辑包含在 Struts 操作中。将业务逻辑放在会话 Bean EJB 中会使提高代码的可重用性以及减弱不同层间的耦合。

如果将业务逻辑与 Struts 操作分离,则:

  • StrutsTestCase 更大程度上变成了一个黑盒测试工具。测试操作时无需您了解后台的运行情况。
  • 这对于直接测试会话 Bean 以便执行白盒测试非常有用。

因为会话 Bean 具有远程接口,所以使用 JUnit 测试会话 Bean 实际上非常简单。JUnit 可以将会话 Bean 用作普通的 Java 对象,因此要调用这些对象无需额外框架。代码如下所示:

package jdubois.otn.ejb.test;

import javax.naming.InitialContext;
import javax.rmi.PortableRemoteObject;
import junit.framework.TestCase;

public class SimpleEjbTest extends TestCase {

protected SimpleEjbHome ejbHome;
protected SimpleEjb simpleEjb;
private String name = "ejb/remote/simplebean";

public SimpleEjbTest() throws Exception {
InitialContext context =
new InitialContext(System.getProperties());

Object obj = context.lookup(name);
ejbHome =
(SimpleEjbHome)PortableRemoteObject.
narrow( obj, SimpleEjbHome.class);

simpleEjb = ejbHome.create();
    }

public void testSimpleMethod () {
try {
String result = simpleEjb.sayHello();
assertEquals("Hello", result);
} catch (Exception e){
fail("An exception has occured "
+ e.getClass()
+ " : "+e.getMessage());
        }
    }
}

与第一个示例 (Message JavaBean) 一样,可在 JUnit 中直接使用此代码。

使用 JDeveloper 调试应用程序。测试用例失败时,使用调试器可以帮助找出问题的所在。Oracle JDeveloper 提供的调试器与当前的 JUnit 方式可以很好地结合,即使是测试服务器端组件时也是如此。

为了使用该调试器,请选择一行 Struts 操作,调试从此处开始。单击该行左侧,出现一个小红点。然后右键单击“login.jsp”,选择“debug”而非“run”。如果启动了 JUnit 测试(例如,使用 AllTests.java 测试套件以及 JUnit GUI),该测试将在选择的断点处暂停,JDeveloper 将进入调试模式(参见图 6):

图 6
图 6:使用 Oracle JDeveloper 和 JUnit 进行调试

Oracle JDeveloper IDE 使用集成的 OC4J 应用服务器,它是在 JDeveloper 中测试和调试应用程序的最简单方式。JDeveloper 使用 Java 的标准测试功能来实现此特性,因此它可以调试任何标准的 Java 应用服务器。例如,在 JDeveloper 中调试在 JBoss 应用服务器上部署的应用程序的方法文档。

用 Ant 使测试自动化。
<cactus/> 任务。 <cactus/> ant 任务能够自动启动应用服务器并自动使用它执行 Cactus 测试用例。如果您要设置项目的连续集成,则这是一个必须使用的任务。

可惜的是,OC4J 并不是一个可在 <cactus/> 任务中使用的应用服务器,但是只要编写了标准的 war/ear 文件,即可使用其他应用服务器来实现此目的。根据 Cactus 文档 (http://jakarta.apache.org/cactus/integration/ant/task_cactus.html#generic) 的说法,可以同时使用 OC4J 和 <cactus/> 任务。然而,OC4J 不是一个受支持的平台,并且以作者的经验,停止 OC4J 时可能会遇到问题 - 将对连续集成过程产生重大破坏。

<mkdir dir="results"/>
<cactus warfile="./cactified-discussion-board.war" fork="yes"
		failureproperty="test.failure"> 
      		
<classpath refid="classpath"/> 
<containerset timeout="300000"> 
<jboss3x dir="${jboss.install.dir}" port="8080"/> 
</containerset> 
<formatter type="brief" usefile="false"/>
<batchtest todir="results"> 
<fileset dir="${src.dir}"> 
<include name=" jdubois/otn/cactus/test/AddMessageActionTester.java"/> 
<include name=" jdubois/otn/cactus/test/LoginActionTester.java"/> 
</fileset>
</batchtest>
</cactus>
<fail message="Cactus in-container tests failed." if="test.failure" />
<junitreport/> 任务。 <junitreport/> 是一个标准的 Ant 任务,它创建 JUnit 测试结果的 HTML 报告。

配置 JUnit,使其生成测试的 XML 报告。为此:

  • 将 <formatter type="xml" /> 子元素添加到 JUnit 或 Cactus 任务中。
  • 添加下列任务生成报告:
    <junitreport todir="results">
    <fileset dir="results">
    <include name="TEST-*.xml"/>
    </fileset>
    <report todir="results" format="frames"/>
    </junitreport>

如果您在一台专用计算机上使用连续集成,则可以创建一个包含所有 JUnit 测试的 HTML 报告;配置您的 Web 服务器,以便所有团队成员都能够访问测试结果的完整集合。

设置“连续集成”。连续集成是极端编程方法的一部分。通过连续集成,可以自动运行测试 — 每天好几次。对于本作者当前的项目,测试每小时运行一次。如果发现错误,错误报告就会以电子邮件的方式发送给在这个期间提交代码的团队成员。一个很好的做法就是,当某人打断连续集成时,其最紧要的任务就是更正其代码,修复该编译版本。

连续集成可以完成更多的任务,而不只是运行 JUnit 测试。例如,它可以运行 Checkstyle 测试,以确保所有开发人员都在遵守该项目的编码准则(另一个极端编程的好做法)。Checkstyle 是一个 SourceForge 项目:http://checkstyle.sourceforge.net/。

有几个连续集成工具可以使用:

  • CruiseControl:http://cruisecontrol.sourceforge.net/
  • AntHill:http://www.urbancode.com/projects/anthill/
  • DamageControl:http://damagecontrol.codehaus.org/

作者使用 CruiseControl 已经很长时间了,真诚推荐该工具。这是一个很棒的工具 - 设计巧妙,设置简单。

结论

JUnit、服务器端测试、测试先行开发。. . . 虽然这些内容都非常复杂,但是如今开发人员可以使用很多工具来帮助突破这些技术难题。另外,这些工具与 Oracle 技术(如 JDeveloper 和 OC4J)集成地非常好。

那就现在就开始测试吧!它使您对您的应用程序更加放心,它给了您更改代码的勇气,而且它充满乐趣

 

版权所有:UML软件工程组织