技术开发 频道

实战每晚构建之主要技术

【IT168 技术文章】

    本文是实战每晚构建系列的第二篇,主要叙述在设计构建平台时要考虑的一些开源或第三方技术,其中既有有类似于"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/

0
相关文章