J2EE开发平台的软件测试技术作者:佚名(来自:cybercorlin.net)
前言 - 以测试为导向的软件开发流程 软件开发流程的新兴观念是将软件测试的角色,提升为系统开发时每一个阶段都必须要持续且反复进行的重要任务,确保每一个阶段都能及早发现潜藏于系统内的危险因子。当某一个阶段的测试结果无法达到预期的要求时,就必须回溯到之前的开发阶段,再次分析和审核,这种过程称之为重构(Refactoring)。配合重构的机制,让系统的品质都能够在严密的测试监控下持续成长。不过由于网络时代的革命兴起之后,软件系统的架构变得更为复杂,相对的软件测试的发展也更显得重要。
J2EE平台内的软件测试
XP只定义了两种测试的层级,第一种层级是「单元测试」,因为单元测试的用意是为了检验程序代码是否合乎逻辑,而且是针对系统内部的模块来测试,因此又可以称做为程序逻辑测试(Code Logic Testing)。为了因应不同的应用程序开发平台的特殊架构,在J2EE的平台里,还发展出与J2EE Container 紧密结合的整合测试(Integration Testing)。
另一种层级是接受度测试(Acceptance Testing),又称作功能测试(Functional Testing)。在软件测试中还有一个大家常听到的是效能测试(Performance Testing)。由于效能测试与客户的需求是密不可分,所以将它归类为接受度测试的延伸应用。最后归纳起来,一个J2EE平台可能所需要的测试流程,以及测试之间的关系,如图一所示。
在Web层内软件测试的概念与流程,如图二所示,其中键头旁的数字符号代表着整个测试流程的执行步骤。首先先准备好受测数据与受测系统之后,借着虚拟浏览器来发出request,向受测系统取得包裹着HTML code的response。然后再利用测试平台来协助我们进行受测数据与预期值的比对工作。当比对后所回报的结果都是正确无误时,代表着受测系统的功能可以正常运作了。图中的测试平台与虚拟浏览器在测试中扮演着关键的角色。
虽然发展测试平台的概念已经行之有年了,然而具备有可延伸且开放式架构的测试平台并不多,其中OpenSource社群以Java开发出来的JUnit,是极具代表性的测试平台。
1. 单元测试平台─ JUnit JUnit平台的设计架构是采用了命令(Command)和复合(Composite)两种设计模式(Design Pattern)做为关键的组成架构。在JUnit平台中的核心类别是TestCase,而每一个TestCase代表着一个命令对象。TestCase包含数个test method,用来测试被测类别内public method的产出对象与预期的结果是否相同。在JUnit平台内有提供数种用来协助比对的assert method。
JUnit平台里还有另一个核心类别是TestSuite,而每一个TestSuite代表着一个复合的对象。一个TestSuite可以由数个TestCase或是数个TestSuite组成,因此可以根据测试的需求,拼凑出多个的TestSuite。整个JUnit测试平台的组成架构,如图三所示。在了解了JUnit平台的架构之后,我们便可以运用JUnit平台来发展受测系统的整合测试与功能测试。
代码实例:
import junit.framework.*;
import java.util.Vector;
public class VectorTest extends TestCase {
protected Vector fEmpty;
protected Vector fFull;
public VectorTest(String name) {
super(name);
}
public static void main (String[] args) {
junit.textui.TestRunner.run (suite());
}
protected void setUp() {
fEmpty= new Vector();
fFull= new Vector();
fFull.addElement(new Integer(1));
fFull.addElement(new Integer(2));
fFull.addElement(new Integer(3));
}
public static Test suite() {
return new TestSuite(VectorTest.class);
}
public void testCapacity() {
int size= fFull.size();
for (int i= 0; i < 100; i++)
fFull.addElement(new Integer(i));
assertTrue(fFull.size() == 100+size);
}
public void testClone() {
Vector clone= (Vector)fFull.clone();
assertTrue(clone.size() == fFull.size());
assertTrue(clone.contains(new Integer(1)));
}
public void testContains() {
assertTrue(fFull.contains(new Integer(1)));
assertTrue(!fEmpty.contains(new Integer(1)));
}
public void testElementAt() {
Integer i= (Integer)fFull.elementAt(0);
assertTrue(i.intValue() == 1);
try {
Integer j= (Integer)fFull.elementAt(fFull.size());
} catch (ArrayIndexOutOfBoundsException e) {
return;
}
fail("Should raise an ArrayIndexOutOfBoundsException");
}
public void testRemoveAll() {
fFull.removeAllElements();
fEmpty.removeAllElements();
assertTrue(fFull.isEmpty());
assertTrue(fEmpty.isEmpty());
}
public void testRemoveElement() {
fFull.removeElement(new Integer(3));
assertTrue(!fFull.contains(new Integer(3)) );
}
}
import junit.framework.*;
import junit.runner.BaseTestRunner;
public class AllTests {
public static void main(String[] args) {
junit.textui.TestRunner.run(suite());
}
public static Test suite() {
TestSuite suite= new TestSuite("Framework Tests");
suite.addTestSuite(ExtensionTest.class);
suite.addTestSuite(TestCaseTest.class);
suite.addTest(SuiteTest.suite()); suite.addTestSuite(ExceptionTestCaseTest.class);
suite.addTestSuite(TestListenerTest.class);
suite.addTestSuite(ActiveTestTest.class);
suite.addTestSuite(AssertTest.class);
suite.addTestSuite(StackFilterTest.class);
suite.addTestSuite(SorterTest.class);
suite.addTestSuite(RepeatedTestTest.class);
suite.addTestSuite(TestImplementorTest.class);
if (!BaseTestRunner.inVAJava()) {
suite.addTestSuite(TextRunnerTest.class);
if (!isJDK11())
suite.addTest(new TestSuite(TestCaseClassLoaderTest.class));
}
return suite;
}
static boolean isJDK11() {
String version= System.getProperty("java.version");
return version.startsWith("1.1");
}
}
2 整合测试的观念与Cactus应用 整合测试提供了J2EE Container的环境,可以快速轻易地检验出Domain Object与J2EE Container的互动行为是否合乎逻辑。因此整合测试的对象是以一个EJB、Servlet或是JSP的程序代码为基本单元。Open Source社群的Jakarta计划中的子计划Cactus,即是为了实作整合测试用的平台而诞生的。
Cactus基本上也是延伸JUnit平台而发展出来的,因此它除了原有基本的method之外,还提供了可以用来模拟浏览器的内部行为的beingxxx( )和endxxx( )的method。这两个method来这些method的执行顺序和与Web Container互动的行为模式,如图四所示。
我们利用beginxxx( )来设定要传递给受测对象的字符串参数。执行完beginxxx( )后,会发出request将参数名称与参数值传递到Web Container。TestCase会执行setUp( ),将受测对象所需要的对象环境建立起来,接着在testxxx( )执行存取受测对象的动作。当存取受测对象的动作执行完后,便可以检验受测对象可能存放在session的产出物。然后在Web Container会执行释放资源的动作,然后将response回传到Client端。最后在Client端执行endxxx( )来进行比对HTML code是否和预期值相同,执行完endxxx()时也代表一个整合测试的结束。将这五个method所执行的功能汇整如表一所示。
虽然Cactus架构提供了受测对象产出物与预期结果的比对功能,但是当回传的HTML code的内容过于庞大复杂时,反而不利于比对的工作。因此采用了一个实用性的做法。此做法是在JSP或servlet欲产出的HTML code的程序代码里,于关键的卷标内添加ID这种属性。当endxxx( )要进行比对前,先读取记载着ID属性值与预期值的外部数据文件,再透过DOM的存取机制来取得HTML code,便能够快速地比对关键的数据。不仅可以将比对的工作模块化,更能够在不需要重新编译测试码的情形下,随时变更预期值。读者们若有遇到相似的问题时,不妨可以采用与相同的策略来解决。
整合测试不同于单元测试,虽然减低了撰写测试码的困难度,但也因为Domain Object与J2EE Container的结合,而不能为Domain Object提供单纯的测试环境。因此若有其它的测试可以单纯地检验整个系统,便可以弥补整合测试的不足。功能测试即是扮演这样的一个角色。
配置信息与代码实例:
<servlet>
<servlet-name>ServletRedirector</servlet-name>
<servlet-class>
org.apache.cactus.server.ServletTestRedirector
</servlet-class>
<init-param>
<param-name>param1</param-name>
<param-value>value1 used for testing</param-value>
</init-param>
</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>
实例testcase:
import junit.framework.Test;
import junit.framework.TestSuite;
import org.apache.cactus.ServletTestCase;
import org.apache.cactus.WebRequest;
public class TestSampleServlet extends ServletTestCase
{
public TestSampleServlet(String theName)
{
super(theName);
}
public static Test suite()
{
return new TestSuite(TestSampleServlet.class);
}
public void beginSaveToSessionOK(WebRequest webRequest)
{
webRequest.addParameter("testparam", "it works!");
}
public void testSaveToSessionOK()
{
SampleServlet servlet = new SampleServlet();
servlet.saveToSession(request);
assertEquals("it works!", session.getAttribute("testAttribute"));
}
}
3 业务逻辑测试与StrutsTestcase应用 struts的使用越来越广泛,但是并没有合适测试框架与之对应.cactus, httpunit虽然都可以测试jsp,servlet,但基于struts框架的应用程序的测试依然比较麻烦。
不过,StrutsTestCase的出现多少解决了些问题,下面主要讲讲StrutsTestCase的应用。和其他的JSP/SERVLET测试框架一样,StrutsTestCase也有两种测试结构,一种是Mock结构,另一种是利用Cactus的结构。
因为StrutsTestCase也是基于junit上开发的,所以它的使用方法也类似于junit.可以参考他的例子和api.
可惜的是StrutsTestCase例子里没有使用ant,因此它的运行较让人头痛,特别是有关配置文件的处理。StrutsTestCase在运行时必须能够找到struts-config.xml和web.xml文件,默认的位置是web-inf/目录下,所以web-inf目录要存在于classpath或者在运行的时候指定。例如:我的位于d:/struts-test/下,struts-config.xml和web.xml位于d:/struts-test/webapp/WEB-INF/下,那么在运行测试的时候应当把d:/struts-test/webapp路径放到classpath里。如果我的struts-config.xml不在/web-inf目录里,而是在/web-inf/conf/目录下,那么就须调用setConfigFile(String path)方法,按照刚才的情况,
public void testSuccessfulLogin() {
setRequestPathInfo("/login"); addRequestParameter("username","deryl"); addRequestParameter("password","radar");
actionPerform();
verifyForward("success"); }
public void testFailedLogin() {
addRequestParameter("username","deryl");
addRequestParameter("password","express");
setRequestPathInfo("/login");
actionPerform();
verifyForward("login");
}
public void setUp() throws Exception {
super.setUp() ;
setConfigFile("/WEB-INF/conf/struts-config.xml") ;
}
4 功能测试的观念与HttpUnit应用 以UML的术语来说,功能测试的对象是检验Use Case所规范的行为,测试系统是否符合所需要的功能,是否能达到使用者的需求?而单元测试的对象是检验对象Classes Diagram与Sequence Diagram所描述的关系与行为,测试单元是否执行正确,是否符合程序逻辑?。每当完成一个阶段性的功能测试,也代表着完成了一部分的系统实作。
Open Source社群的HttpUnit API套件,即是为了功能测试而发展出来的。HttpUnit是以Java撰写出来的虚拟浏览器,用来模拟浏览器的内部行为。前一节所提到的Cactus检验HTML code的机制,也是采用HttpUnit来完成的。
除此之外,HttpUnit还可以结合JUnit平台撰写测试码来检验回传的网页内容是否与预期结果相符合。HttpUnit平台的运作机制是建构在Http标准通讯协议之下,藉由模拟使用者浏览网站时,所发出的以对象的形式封装的request讯号,将其送至到目的网站,然后等到该网站处理完此request之后,便将同样以对象形式封装的response讯号回传给HttpUnit。
由于HttpUnit所接收的是标准HTTP协议的response对象,因此不论该网站是静态网页语言或是用任何的动态服务器端语言写成的,都可以透过HttpUnit来模拟网站浏览的行为并且取得标准的HTML code。
市面上也有提供功能测试用的预录播放软件,可以事先录下网站浏览的步骤,然后反复地播放预录好的流程,最后回报测试的数据给测试人员,供测试人员进行分析,大大节省撰写测试码的负担。然而此类软件有以下的缺点:
1. 当网站设计的复杂度越高,浏览的分支流程越多,预录好的流程便无法作有效的模块化管理。
2. 预录播放软件虽然有提供记录浏览步骤的script或是XML文件,虽然这些指令码可以重复利用,然而若无法提供有效的侦错机制,一旦安插了错误的程序,反而容易造成无法预期的错误产生。
3. 测试人员需要重新学习专属于预录播放软件的script语言或是XML文件语法,无法从既有熟悉的程序语言来编写浏览网站的程序。
4. 当网站的操作接口时常为了需求而新增或是修改原有的互动设计时,必须重新录制新的浏览网站的程序,而无法重复利用。
HttpUnit解决了软件开发人员以上的困扰。HttpUnit是一种黑箱作业形式的测试工具,因此我们只要专注如何在JUnit平台上撰写模拟浏览器行为的测试码即可。HttpUnit内的method执行顺序和与Web Container互动的行为模式,如图五所示,箭头符号旁所标示的数字,代表着这些method的执行顺序。
当我们在setup( )设定好受测的网址与相关的环境后,setUp( )会执行向受测网址进行存取的动作。当存取动作完成时,会将response回传至Client端。此时可以在testxxx( )做HTML code和预期值比对的工作。最后在Client端执行释放资源的动作,执行完tearDown()时也代表一个功能测试的完成。将这三个method所执行的功能汇整如表二所示。
虽然HttpUnit提供强大的仿真功能,但是HttpUnit本身还是存在两个缺点。第一,当HttpUnit结合JUnit平台做测试时,由于HttpUnit存取HTML code的方式与HTML内部的文件结构的关联过于紧密,因此当网页版面需要变动时,也需要修改相对应的测试码。对于这样的困扰,采用了与Cactus检测HTML code同样的改良策略,来达到快速比对而不用调整测试码的好处。
第二,在HttpUnit与JUnit平台结合做测试的情况时,由于JUnit特殊的运作机制,无法记住每一个已经浏览过的网址状态,因此当某个受测网址与其它网址的依存性强时,若要回传正确的浏览状态时,必须要用递归记忆的方式来达成。例如要存取第二个网页必须记住第一个网页的状态,存取第三个网页要记住第一个和第二个网页的状态,同理存取第n个网页时需要记住第一个网页到n-1个网页,这样的做法不易将测试码模块化,如图六所示。
于是利用了HttpUnit本身也可以写成独立运作的程序代码的特性,写成一个浏览网站步骤的仿真器。然后利用JUnit的setUp( )来存取受测网站的浏览状态,便可以在testxxx( )取得正确的网页状态来进行比对的工作,经过模块化后的HttpUnit测试架构如图七所示。
5 可以参考的测试项目:Junit 包中有比较简单的测试用例的例子。
Eclipse 开发平台的 JUnit Plugin Tests and Automated Testing Framework 插件中的JunitTest项目例子,比较详细的介绍了junit测试平台。
参考文献
1. JUnit官方网站http://www.junit.org/index.htm
2. Cactus官方网站http://jakarta.apache.org/cactus/index.html
3. Strutstestcase网站http://strutstestcase.sourceforge.net/
4. HttpUnit 网站 http://sourceforge.net/projects/httpunit/
5. Eclipse 网站 http://www.eclipse.org/downloads/index.php