UML软件工程组织

实战每晚构建(中)
文章出处:不祥  作者:龚永生

内容:

1 相关开源或第三方技术
 1.1 ant 项目构建工具
 1.2 junit单元测试
 1.3 cactus单元测试
 1.4 clover测试覆盖率计算
 1.5 statcvs项目度量工具
 1.6 velocity模版系统
 2 文档书写辅助工具
 参考资料
 关于作者

本文是实战每晚构建系列的第二篇,主要叙述在设计构建平台时要考虑的一些开源或第三方技术,其中既有有类似于"Hello world"的入门介绍,也有精髓内容解析,还有注意点提醒。

 1、相关开源或第三方技术

 在进行设计之前,我们有必要了解一些开源或第三方在项目构建方面的技术。学习这些技术的最好方式是弄到一份,仔细阅读文档,实践一些小的例子,在工作当中使用之。

1.1 ant 项目构建工具

为了让大家更好地了解后面的设计,本节出了介绍基本知识外,还介绍了这个工具的主要特点中的三点:多个文件组成配置文件,目标依赖性,扩展,另外讲述了ant配置脚本的面向对象特性。

简述
 Ant是Apache开源运动中的一份子,它是和大家所熟悉的Make系统一样的基于Java的构建工具。他克服了Make的os依赖性,同样也可以调用os特有的指令。不像Make使用操作系统的脚本命令,ant使用java 类来扩展自身。Ant的配置文件是流行的xml格式。

下面是一个最简单的build.xml文件:

<?xml version="1.0" encoding="ISO-8859-1"?>
<project name="projectTemplate" default="init" basedir=".">
<target name="init" >
<property name="lib.dir" value="lib"/>
<echo message="Hello ${user.name}! lib.dir is set to ${lib.dir}" >
</echo>
</target>
</project>

运行ant命令将产生下面的结果:

gongys$ ant
gongys$ Hello gongys! lib.dir is set to lib

在这个简单的build.xml显示了ant配置文件定义目标(target),定义属性(property),访问属性的方法,其中${user.name}是个系统属性。

多个xml文件定义ant配置文件
 下面我们给出一个相对复杂的build.xml文件:

<?xml version="1.0" encoding="ISO-8859-1"?>
<!DOCTYPE project [
<!ENTITY build-abstract SYSTEM "file:./build-abstract.xml">
]>
<project name="projectTemplate" default="init" basedir=".">
<target name="init" depends="init.variables">
<property name="lib.dir" value="lib"/>
<echo message="Hello ${user.name}! lib.dir is set to ${lib.dir}" />
<echo message="build.dir is set to ${build.dir} in build-abstract.xml " >
</echo>
</target>
<target name=" clean" depends="init" >
<del dir="${build.dir}"/>
</target>
&build-abstract;
</project>

其中<!ENTITY build-abstract SYSTEM "file:./build-abstract.xml">定义了一个名为build-abstract的实体,其内容为当前目录下的build-abstract.xml文件。&build-abstract;引用了这个实体,这样在build.xml文件就可以用build-abstract.xml定义的目标啦。

下面是build-abstract.xml的内容:

<target name="init.variables">
<property name="build.dir" value="tempbuild"/>
</target>

开发和定义自己的task
 ant是一个可以扩充的构建工具,开发者可以开发自己的java类来扩充ant。下面是一个简单的扩充类:

package com.mydomain;
import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.Task;
public class MyVeryOwnTask extends Task {
private String msg;

// The method executing the task
public void execute() throws BuildException {
System.out.println(msg);
}

// The setter for the "message" attribute
public void setMessage(String msg) {
this.msg = msg;
}
}

这个扩展任务将有一个属性message,ant在执行这个任务时会调用execute方法。下面是在build.xml配置文件中使用这个扩展的示例:

<?xml version="1.0"?>
<project name="OwnTaskExample" default="main" basedir=".">
<taskdef name="mytask" classname="com.mydomain.MyVeryOwnTask">
<classpath>
<pathelement location="where/u/put/the/class/"/>
</classpath>
<target name="main">
<mytask message="Hello World! MyVeryOwnTask works!"/>
</target>
</project>

目标依赖性
 了解ant的另外一点是target的依赖性,上面这个比较复杂一点的build.xml的依赖性如下图所示:
 
 这样的依赖图使得执行命令ant init 时先执行init.variable目标中的指令,执行clean目标时先执行依次执行init.variables和init目标。

到目前为止,还没有哪一个集成工具开发出自动分析ant配置文件依赖性图的插件,但是命令行下已经有了。

这个工具名叫vizant,也就是一个实现了扩展ant任务的jar文件,还包含了一些文档和例子,下面是我产生上面的目标依赖图的build.xml

<?xml version="1.0"?>
<!-- $Id: build.xml,v 1.1 2003/04/29 10:25:12 gongys Exp $ -->
<project name="Vizant" basedir="." default="dot">
<property name="build" location="output"/>
<property name="vizant.antfile" value="${buildfile}"/>
<property name="dot.format" value="png"/>
<target name="init">
<echo message="${vizant.antfile}" />
<tstamp/>
<mkdir dir="${build}"/>
</target>
<target name="defvizant">
<taskdef name="vizant" classname="net.sourceforge.vizant.Vizant" classpath="vizant.jar"/>
</target>

<target name="vizant" depends="defvizant,init">
<vizant antfile="${vizant.antfile}" outfile="${build}/build.dot" uniqueref="true"/>
</target>

<target name="dot" depends="vizant">
<exec executable="${basedir}/dot.exe" v
<arg line="-T${dot.format} ${build}/build.dot -o ${build}/out.${dot.format}"/>
</exec>
</target>
</project>

你在要分析的项目目录下执行如下命令便可在output/out.png的依赖图形文件。

gongys$ ant -f vizant/build.xml -Dbuildfile=build.xml

-f vizant/build.xml定义了ant配置文件,-Dbuildfile=build.xml定义了要分析的ant配置文件。

Ant配置脚本的面向对象性
 从上面可以知道一个ant的配置脚本可以由多个配置文件组成,一个配置文件由目标和属性定义语句组成。我们可以把属性看成是面向对象中的成员变量,目标看成是方法,这样一个配置文件就定义了一个"类",而且它的成员都是静态的,就是说不需要生成"对象"。一个类是可以运行的如果它的配置文件的顶级元素是<project>,这就好像我们的java类实现了public static void main(String[] args)方法一样。可以用xml中的定义和引用实体的方式来申明一个"类"继承了另一个"类",这样我们可以实现面向对象当中的"类继承层次图";我们可以用<ant>任务来实现跨对象之间的调用(要求这些对象的类是可以运行的),这样就形成了"对象协作图";我们可以用<antcall>和目标的depends属性来实现对象内部的"方法调用"。

注意Ant配置脚本的面向对象模型没办法实现方法重载或覆盖。

1.2 junit单元测试
大部分集成工具都集成了junit单元测试插件,并有向导帮助写单元测试。Junit发行包的文档很详细地介绍了Junit的设计概念和所使用的设计模式。在这里我简单地说明如何写测试用例、在ant配置文件中调用测试用例和产生测试报告的方法。

写测试用例
 下面是在eclipse junit向导对MyCode类编写的测试用例TestMyCode文件基础上写的代码:

import junit.framework.TestCase;

/*
* Created on 2003-4-30
*
* To change the template for this generated file go to
* Window>Preferences>Java>Code Generation>Code and Comments
*/

/**
* @author gongys
*
* To change the template for this generated type comment go to
* Window>Preferences>Java>Code Generation>Code and Comments
*/
public class TestMyCode extends TestCase {
MyCode myFixture=null;
/**
* Constructor for TestTest.
* @param arg0
*/
public TestTest(String arg0) {
super(arg0);
}
/*
* @see TestCase#setUp()
*/
protected void setUp() throws Exception {
super.setUp();
myFixture = new MyCode();
System.out.println("setup");
}

/*
* @see TestCase#tearDown()
*/
protected void tearDown() throws Exception {
super.tearDown();
myFixture = null;
System.out.println("teardown");
}

public void testSetName() {
myFixture.setName("gongys")
assertEquals("gongys", myFixture.getName());
System.out.println("testSetName");
System.out.println(this.getName());
}
public void testSetAge() {
System.out.println("testSetAge");
myFixture.setAge (12)
assertEquals(12,myFixture.getAge());
System.out.println(this.getName());
}
}

有几点需要特殊指出:

一个TestCase子类中可以包含多个test方法,test方法的原型必须是public void testXXX();
 在执行过程中,junit框架为每一个test方法实例化一个TestCase子类;
 执行testCase的顺序如下:setUp(),testXXX(),teardown();
 fixture是指为每一个测试方法准备的东西:比如数据库连接,此时的目标等,一般在setUp()中设置,testXXX()中使用,teardown()中释放。

 运行这个测试的结果如下:

setup
testSetName
testSetName
teardown
setup
testSetAge
testSetAge
teardown

ant使用测试用例
 利用ant 的junit任务和其子任务test可以在ant配置文件中执行单元测试,如下所示:


<target name="outter_unittest" depends="init">
<junit printsummary="yes" fork="yes" haltonfailure="no" >
<classpath>
<fileset dir="${build.dir}">
<include name="TestMyCode.class" />
<include name="MyCode.class" />
</fileset>
<pathelement location="${lib.dir}/${junit.jar}"/>
</classpath>
<formatter type="xml"/>>
<!--this specify the output format of junit -->
<test name="TestMyCode" todir="tempjunit" />>
<!--this will run all testXXX methods of the TestMyCode and generate the output to dir tempjunit , the output file is TEST-TestMyCode .xml -->

</junit>
</target>

需要注意的是:

要正确设置junit任务的classpath子元素,classpath至少要包含三样东西,TestCase子类比如TestMyCode,你测试的代码的java类比如MyCode,和junit.jar;
 可以使用formatter子元素设置junit任务中test任务的输出的格式;
 test任务可以设置输出文件的名字和目录;
 junit任务还有一个子任务batchtest可以用通配符来指定TestCase子类。

 Ant中生成测试报告

 在上面的一节中我们谈到junit任务可以生成测试结果,并输出到指定的文件和目录中,在ant中,我们还可以用junitreport任务对这些测试结果进行处理,生成html文件:

<junitreport todir="./tempjunit ">
<fileset dir="./tempjunit ">
<include name="TEST-*.xml"/>
</fileset>
<report format="frames" todir="./report/html"/>
</junitreport>

junitreport任务首先把fileset中指定的测试结果归集成一个xml文件,接着用子任务report转化成html文件,子任务report的format属性指定生成的结果是框架的还是没框架的。

1.3 cactus单元测试

 cactus单元测试工具是对junit框架的扩充,使junit的思想和便利同样用于Browser/Server web应用程序中的测试,具体的来说就是测试servlet,jsp和filter。 本节讲述cactus 单元测试原理,servlet测试用例的书写(jsp,filter的测试用例的书写请参照cactus文档),如何配置ant运行这样的测试。

cactus 单元测试原理
 
 Cactus提供了好几个扩展JUnit Testcase的子类和相应的redirector,上面的工作原理图解释了cactus测试的工作原理。

其中YYYTestCase = ( ServletTestCase子类 | FilterTestCase子类 | JspTestCase 子类)

XXX我们写的testcase名字的后半部分。

下面我们分步骤解释在我们的cactus Testcase子类里头的每一个testXXX()方法的具体情况:

  • JUnit 测试运行器调用YYYTestCase.runTest()方法。
  • 这个方法寻找 beginXXX(WebRequest)方法,如果找到则执行。
  • 传给beginXXX(WebRequest)方法的参数WebRequest 可用来设置 HTTP头, HTTP 参数,这些参数将被发送到第2步的 Redirector 代理。
  • YYYTestCase.runTest() 方法打开连向Redirector 代理的HTTP 连接,beginXXX(WebRequest)方法设置的HTTP协议参数将被送到代理。
  • Redirector 代理在服务端作为YYYTestCase的代理(其实我们的YYYTestCase被实例化两次,一次在客户端被JUnit 测试运行器实例化,一次在服务器端被代理实例化,客户端实例执行beginXXX() and endXXX()方法,服务端实例执行Junit 测试用例的方法setup(),testXXX(),and teardown())。
  • Redirector 代理有下列事情可做:
  • 用java的内省功能创建服务端实例;
  • 设置一些缺省对象;
  • 按照客户端实例的意愿创建session。
  • 执行Junit 测试用例的方法setup(),testXXX(),and teardown();
  • 我们的 testXXX()方法调用服务端代码来进行测试,使用assertEquals()方法对测试结果和预期结果进行比较,如果两者相符为测试成功,否则为测试失败;
  • 如果测试失败,Redirector 代理将捕获testXXX()方法抛出的的异常;
  • Redirector 代理将异常信息返回给客户端的JUnit 测试运行器,JUnit 测试运行器可以生成测试报告;
  • 如果没有异常出现, YYYTestCase.runTest()方法寻找
  • endXXX(org.apache.cactus.WebResponse) endXXX(com.meterware.httpunit.WebResponse) (后者用在和httpunit集成中) 方法,如果找到则执行。
  • endXXX方法中,我们可以检查返回的HTTP 头, Cookies 和output stream ,这个检查可以借助于Junit的 assertEquals或者cactus提供的帮助类。

    在这里需要提出的一点就是:代理不会去真正执行servlet,或filter,或jsp的代码,你需要在testXXX方法中调用或模仿这些代码。

书写servlet测试用例

import java.io.IOException;
import junit.framework.Test;
import junit.framework.TestSuite;
import org.apache.cactus.ServletTestCase;
import org.apache.cactus.WebRequest;
import org.apache.cactus.WebResponse;

public class TestSampleServlet extends ServletTestCase
{
public TestSampleServlet(String theName)
{
super(theName);
}

public static Test suite()
{
return new TestSuite(TestSampleServlet.class);
}
//这个方法在服务端运行,用来设置fixture
public void setup(){
}
//这个方法在服务端运行,用来释放fixture
public void teardown(){
}

//这个方法在客户端运行,可以用来设置请求参数
public void beginSaveToSessionOK(WebRequest webRequest)
{
webRequest.addParameter("testparam", "it works!");
webRequest.setURL("localhost", "test", "SampleServlet" ,"gongys", "name=gongys");
}
//这个方法在服务端运行,用来具体进行代码测试

public void testSaveToSessionOK() throws IOException
{
SampleServlet servlet = new SampleServlet();
servlet.saveToSession(request);
System.out.println(this.request.getPathInfo());
System.out.println(this.request.getParameter("name"));
this.response.getWriter().println("gongys");
assertEquals("it works!", session.getAttribute("testAttribute"));
}
//这个方法在客户端执行,用来验证返回结果
public void endSaveToSessionOK(WebResponse theResponse){
System.out.println(theResponse.getText());
}
}

配置ant运行cactus测试

类路径的设置
 我们要按照下面的图设置客户端(ant junit任务中)设置classpath,并把右半部分所示的类放到服务器或者webapp的类路径上
 
 客户端cactus.properties
 我们知道,cactus需要redirector 代理才能工作,我们除了把这些代理考到相应的webapp的类路径(对于filter和servlet代理)或webapp路径(对于jsp代理)外,我们还需要告诉客户端测试实例到哪里去找这些代理,下面是cactus.properties的内容:

cactus.contextURL = http://localhost:8080/test

其中test为被测试webapp的上下文路径。

cactus.properties也必须放在ant junit任务的classpath中。

服务器(假设为tomcat 4.12)server.xml的设置
 我们必须在server.xml中添加cactus redirector代理,使得这些代理能接受客户端测试实例传过来的请求。详细添加办法请参见cactus 文档。

有了正确的junit 类路径的设置,其他的就合正常的junit测试一样。

1.4 clover测试覆盖率计算

clover覆盖率计算工具通过在被测源代码中插入相关指令,在被测源代码被执行时这些指令被执行,用以统计被测源代码被执行的次数,clover利用一个数据库来保存这些数据。Clover还提供了访问这个数据库的工具,并产生html报告文档。

配置ant运行clover分析

clover实现了一些ant任务,下面是ant中定义这些任务的代码

<taskdef resource="clovertasks" >
<classpath>
<pathelement location="${clover.jar}"/>
</classpath>
</taskdef>

下面的代码初始化clover数据库:

<target name="with.clover" depends="init">
<!-- 删除${build.dir}使得重新编译源代码 -->
<delete dir="${build.dir}" />
<mkdir dir="${build.dir}" />
<clover-setup initString="${user.home}/${ANTLOG_FILE_NOEXT}.db" />
</target>

下面的代码产生clover分析,格式为html,结果放在tempcloverreport目录中:

<target name="clover.html" >
<delete dir="tempcloverreport"></delete>
<mkdir dir="tempcloverreport" />
<property name="clover.html" value="ok"<>/property>
<clover-report>
<current outfile="tempcloverreport">
<format type="html"/>
</current>
</clover-report>
</target>
<!-- 下面用一个目标来初始化clover,编译源代码,unittest单元测试和clover分析-->
<target name="clover_report" depends="with.clover, compile,unittest, clover.html">
</target>

这个任务的工作原理为,with.clover在初始化clover数据库后,监视compile;在javac编译java源代码时把记录代码执行的相关指令插入到java源代码中;在单元测试时,这些插入的代码就开始记录被测试代码的执行次数,把结果输出到clover数据库中;clover.html目标根据数据库中的数据生成html文件。

需要注意的几点:

  • 如果是执行cactus类的client/server测试,在服务端的类径中必须包含clover.jar类;
  • clover 是一个商业工具,但可以得到30天的评估license;
  • clover 在编译过程中改变了代码的执行路径,在产品发布时必须单独执行compile目标。
  • Clover 分析结果
    下面是Clover 分析结果的图示,读者可以自己看出从这个分析中能得到什么。第一个图是显示一个项目的整体覆盖率情况,第二个图显示了每一个类每行代码的覆盖情况。

1.5 statcvs项目度量工具

statcvs是一个利用cvs reporsitory log生成项目度量的工具,这些度量包括每个作者的代码量,每个目录或文件的代码行数。使用statcvs先要学会使用cvs。

Ant 中使用cvs
 Ant 中使用cvs是通过cvs任务来完成的:

<property name="cvsroot" value=":pserver:anonymous@10.1.36.135:/data/src" />
<!--取出源代码,放在tmp目录下,相当于执行cvs co -d ${base.path}/tmp/${location} -->
<cvs cvsRoot="${cvsroot}"
package="${location}"
dest="${base.path}/tmp"
/>
<!-- 执行cvs log ,结果放在tmp.log中-->

<cvs dest="${base.path}/tmp/${location}" command="log" output="${base.path}/tmp/${location}/cvs.log"/>

Ant 中使用statcvs
 Statcvs实现了一个ant任务,下面是ant中定义这个任务的代码:

<taskdef name="statcvs" classname="net.sf.statcvs.ant.StatCvsTask">
<classpath>
<pathelement path="${statcvs.jar}"/>
</classpath>
<</taskdef>

下面是使用statcvs任务产生项目度量数据的代码,结果是一些html文件,放在${statcvs.htmldir}目录下:

<statcvs
projectName="${location}"
projectDirectory="${base.path}/tmp/${location}"
cvsLogFile="${base.path}/tmp/${location}/cvs.log"
outputDirectory="${statcvs.htmldir}"
/>

1.6 velocity模版系统

velocity模版系统比起jsp模版来说有比较大的好处:

实现视图和控制代码的完全隔离
 在jsp中,我们可以嵌入执行代码,jsp本质是具有格式化代码和控制代码混合能力,虽然大家发明了好多方法、设计模式和最佳实践,可是不能从根本上消除jsp编写员混合格式化代码和控制代码的恶习;而在velocity模版系统中,这种混合不可能存在,你不可能在velocity的.vm文件中通过代码Person p = new Person()生成一个Java对象,这些业务对象只能在控制中生成并放到context中。

安全
 jsp文件被编译之后形成了一个类似于servlet的东西,几乎可以在jsp中干任何事,你可以在jsp中写 System.exit(0)来关掉java虚拟机,或利用别的什么漏洞。

这里只说这些好处,关于其他的大家可以到网上去查或自己总结。下面我要介绍一下velocity模版系统工作机制和关于velocity的设置问题。

velocity模版系统工作机制
 我们以在servlet环境下的模版系统为例(当然控制还可以由其他代码来实现)。控制可以实例化一些业务对象比如Person 放到context 中(执行context的相关方法),控制在接着装载相关的视图的模版比如PersonInfo.vm,产生Template实例,并让这个实例解释自己生成输出比如html格式流,Template实例在解释模版的时候会根据模版文件中的指令访问context中的业务对象。

所以要使这个模式工作,重要的一点是控制必须和视图就context中的业务对象的名字达成一致,这就是控制和视图的协议。


 velocity的设置
 velocity运行的第一个任务就是初始化,执行Velocity.init方法。无参数的init方法会采用缺省的属性配置,在velocity.jar 中的org.apache.velocity.runtime.defaults.velocity.properties位置;使用有参数的init方法,参数传递的是一个属性文件或java.util.Properties 对象,参数中定义的属性会覆盖缺省的属性设置,没定义的属性会采用缺省的属性设置。

比较有用的属性设置是读取模版文件时采用的字符集、产生输出流时使用的编码、模版所在的位置和模版装载器:

input.encoding = gbk
output.encoding = gbk
file.resource.loader.path = templates

2、文档书写辅助工具

word 文档书写排版工具
 powerpoint,图片组织绘画工具
 visio 绘制数据流图,ER图等的工具
 rational rose,绘制UML图形的工具
 windows 附件中的画图来截取图片
 操作系统的全屏打印功能

 参考资料

进一步学习面向对象的系统分析和设计:《面向对象的系统分析和设计》Ronald J. Norman
 《实用面向对象软件工程教程》殷人昆 田金兰 马晓勤 译
 良好的用例编写风格可以从这里获得:《编写有效用例》 Alistair Cockburm
 进一步理解cvs和nightlybuild技术的相关背景资料:《cvs和nightlybuild技术》 杨锦方
 cvs源代码版本系统在:http://www.cvshome.org
 statcvs 项目工作量分析工具在:http://statcvs.sf.net/
 clover测试覆盖率分析工具在: http://www.cortexebusiness.com.au/
 ant构建工具在:http://ant.apache.org
 junit单元测试工具在:http://www.junit.org
 apache web程序测试工具在:http://jakarta.apache.org/cactus/

 关于作者
 龚永生 (gongys@legend.com)
 北京市海淀区上地信息产业基地开拓路7号联想大厦


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