时 间 记 忆
最 新 评 论
专 题 分 类
最 新 日 志
最 新 留 言
搜 索
用 户 登 录
友 情 连 接
博 客 信 息
 
实战每晚构建(转)
[ 2009/6/18 16:06:00 | By: yanzi ]
 

本文有两个目的:实现每晚构建平台和探讨一个软件从需求文档到设计文档的书写规范。

1、前言

本文有两个目的:实现每晚构建平台和探讨一个软件从需求文档到设计文档的书写规范。

每晚构建是软件研发管理中极具价值的手段,对于加快发现和改正缺陷,降低集成风险,提高产品质量,加强成员沟通与协作,缩短产品上市时间,增加项目开发透明度,提高项目组成员信心和斗志有着非常重要的作用和意义。本文从软件工程过程:需求定义,分析,设计出发描述了实战每晚构建平台的大部分过程。

软件工程中文档有着极其重要的地位,良好的文档风格和习惯是一个团队成熟的重要标志。目前有些软件研发人员特别是刚刚走上岗位的研发人员对文档书写没有一个统一的概念,他们不知道需要写那些文档,他们不知道每个文档需要写到什么地步,一提到写文档就抓耳扰腮。本文试图在这个方面提供一个文档样例,我不认为我的文档写的很好,只希望能起到抛砖引玉之功效。

为了实现这两个目的,本文首先解释了每晚构建,让读者对这个术语有个比较清楚的认识;然后借助于《编写有效用例》中的用例编写技术编写了每晚构建平台的用例并附加了其他相关需求形成了一个相对使用的需求定义文档;接着用面向对象的系统分析方法对需求定义进行了问题空间分析,构造出分析模型;为了读者更好更快地理解设计,后面是一整节的相关开源或第三方技术介绍,有类似于"Hello world"的入门介绍,也有精髓内容解析,还有注意点提醒;接着是平台两大系统的设计。

整个每晚构建平台包括两大子系统:构建系统和构建信息显示系统。构建系统用ANT构建脚本实现;构建信息显示系统是个典型的web应用。两者都采用了面向对象的分析和设计技术。

需求定义的阅读应该以用户为主,文档撰写者为辅,需求定义完成的标志是两者达成了一致;分析章节的阅读应是文档撰写者帮助用户阅读,使用户理解并相信分析模型是能解决用户的问题的;设计章节的阅读主要是系统实现者,除了要求可实现性外,设计模型还要和分析模型有很强的继承性和一致性。

整个文档分为上中下三篇,上篇主要讲述需求定义和分析模型。





回页首


2、每晚构建的定义和作用

构建是从代码库中取出一个开发中的软件项目的所有最新代码,放在一个干净的环境下面,编译源代码文件,连接,安装和测试,并记录整个过程中所有日志的动作系列。

构建平台是一个构建和构建信息展示的系统。

每晚构建主要是指在开发活动不太激烈的情况下,尽量不要干扰开发者的正常开发工作,保证每天执行构建一次。

在《cvs和nightlybuild技术》一书中提到了每晚构建的作用:

每个模块有一部分成果集成一部分成果可以大大地降低集成风险,加强模块间协作性错误的诊断,降低整个系统的不确定性,可以更好地定位错误从而加快开发速度,促使模块间的接口规范而增强团队合作,每天都有新系统新成绩是对每位项目组成员的一个重要激励。

除了这些作用之外,我们还可以从每晚构建中为项目管理提供额外的信息:

每晚构建的产品,每晚构建过程的记录,每晚构造过程中的测试记录,分析测试质量的测试覆盖率分析,构建对象中项目组成员的贡献和项目组成情况分析(本文称为项目度量)。





回页首


3、需求定义

关于需求定义的文档我们一般称之为《需求规格说明书》,这个文档的任务在于比较明确地确定项目的目的和范围,提出功能需求和非功能需求,功能需求也叫行为需求,一般可以用用例来描述,如果所有需求的全集我们称为问题域,则功能需求我们可以称之为问题域的本质。软件研发公司一般都有《需求规格说明书》的框架,规定了书写需求定义文档的"纲",下面就普遍格式对需求进行描述。

1.目的和范围

每晚构建平台是提供一个自动化的信息系统,用来辅助构建者构建系统,尽量做到自动化,最大化地减轻构建者的工作负担;记录构建过程的详细记录,给项目经理、组织层次的软件开发过程改进人员评估和管理软件开发项目提供相关数据,为项目成员和组织领导增加软件项目开发的透明度。

2.术语

构建信息关心者是关心构建过程的记录信息的人员,包括项目经理、过程改进人员、项目成员和组织领导。

3.用例

根据我们前面的构建平台的定义,实现这样的系统主要有两个用例和两种角色。如下图:




  1. 用例一:构建

    名称:构建
    级别:user goal
    范围:每晚构建平台
    主要角色:构建者
    前提:主要角色已登录

    成功场景:

    1. 构建者要求系统从代码库中取出某个项目的所有源代码
    2. 构建者编译连接所有源代码,系统产生项目软件
    3. 构建者要求安装构建出的项目软件,系统安装软件
    4. 构建者测试软件,系统测试并记录测试信息

     

    扩展:
    * 系统出错:
    *.1系统保存出错信息

  2. 用例二:浏览构建信息

    名称:浏览构建信息
    级别:user goal
    范围:每晚构建平台
    主要角色:构建信息关心者
    前提:已执行构建

    成功场景:

    1. 构建信息关心者进入构建信息地址
    2. 系统显示所有的构建信息的目录
    3. 相应信息选择感兴趣的构建信息
    4. 系统显示相应的信息

     

    扩展:
    * 系统出错:
    *.1系统保存出错信息

4.技术

  1. 要求用web技术,采用j2ee体系;
  2. 采用cvs open souce系统作为项目源代码版本控制工具;

  1. 数据字典

    构建信息:构建出的产品;单元测试日志(成功与否,失败则要提供相应失败原因);单元测试的覆盖率;构建过程的日志;项目组成员的工作量(代码行数)和项目源代码树中各个目录和非二进制文件的行数;项目源代码树。

  2. 其他需求
    1. 尽量采用第三方开源代码软件;
    2. 尽量使构建用例自动化;
    3. 能构建多个项目;
    4. 实现用java 语言写的项目的构建,但要考虑未来对c,c++程序的构建的扩展。




回页首


4、系统分析

系统分析要对问题空间的本质进行分析造模,形成"要做什么"的深刻了解,尽可能地和用户达成共识。我这里既使用了传统的造模工具也利用了面向对象分析的方法学来产生文档。这一节中我首先对这些需求所涉及的子系统进行定位,看看他们之间的关系,这里我把这种分析称为上下文分析;然后看看我们关心的子系统数据流如何,得出数据流图;最后我分析每晚构建平台的"类"模型。本节所有内容的全体构成了对需求的分析模型。

1.系统上下文分析




通过分析,我们可以看出整个每晚构建平台的运作由6个实体构成,它们之间的关系如系统关系图所示。

构建系统完成需求定义中的软件项目的构建工作,它从CVS系统中获取项目源代码,并对源代码进行编译连接安装和测试工作,产生的构建信息通知给构建信息展示系统,构建信息关心者通过构建信息展示系统浏览构建信息。用蓝色标记表示的为本平台的组成部分。

2.数据流程图

下面的数据流程图更好地更详细地理解了需求,从这个流程图可知,构建系统主要和CVS系统打交道,并会生成构建信息供构建信息展示系统使用。构建系统由取出项目代码、产生项目度量数据、编译连接安装测试删除和编译连接安装(发布版)四个任务组成;构建信息展示系统和构建信息关心者打交道,主要完成浏览构建信息的任务。用黄色线条表示的存储单元"项目源代码目录"是个临时存储单元。




最后需指出的是本分析模型实现的用例一为全自动版本,是比较符合"最大化的减轻构建者工作负担"的需求的。

类模型

  1. 类关系


    类BuildAdmin,ProjectBuild是project类的子类,因为从某种意义上来说,它们两者都是项目,不把OSScheduler类列为project类的子类是因为OSScheduler类很简单,可能是操作系统的一行脚本或者一行配置。

    另外BuildAdmin负责所有ProjectBuild服务execute方法的执行,所以它和ProjectBuild的关系为一个复合关系;OSScheduler类要启动BuildAdmin的execute方法,所以OSScheduler和BuildAdmin有单向关系。

    另外构建信息存放的位置是本平台的关键,为了让所有的类对这些位置有一个共同的视图,BuildInfoLog类是其它类的父类。

  2. 类描述

    经过分析不难知道,所有的构建信息用非结构化数据来描述比较合适。在这里可以做一个决策:要求他们是独立的,也就是说进入相应的信息地址,它们可以自我显示;需求定义要求用www的方式浏览这些信息,所以同时要求他们对普通的browser是可浏览的。我们没必要用类来描述这些信息。

    构建显示信息系统只有一个类来显示构建信息目录:每晚构建显示类(NightlyBuild)

    另外从上下文图和数据流程图,我们可以得到我们构建系统的类:操作系统定时服务(OSScheduler),构建管理服务(BuildAdmin),应用项目构建服务(ProjectBuild)。

    两个系统之间有一个定义构建信息存放位置的类BuildInfoDir,这个类定义了两个系统之间的协议。

    注意书写约定:
    ${变量名}为取变量名所代表的值的含义。

    1. 构建信息存放位置类
      类名 构建信息存放位置类 类英文名 BuildInfoDir
      成员变量
      变量名 变量说明
      nightly_Build_Tags 保存所有的构建标签,是构建标签列表
      logTopDir 保存构建管理服务运行的日志的目录
      statCVSTopDir 保存了所有应用项目的项目度量结果的顶层路径
      projectLogTopDir 保存了所有应用项目构建服务实例运行日志的顶层路径
      testCoverTopDir 保存了所有应用项目的测试覆盖率计算结果的顶层路径
      distTopDir 保存了所有应用项目的发布版的顶层路径
      testTopDir 保存了所有应用项目的测试结果的顶层路径
      方法
      方法名 参数 执行步骤 方法说明

    2. 每晚构建显示类

      类名 每晚构建显示类 类英文名 NightlyBuild
      成员变量
      变量名 变量说明
      方法
      方法名 参数 执行步骤 方法说明
      list_ Buildinfo_table . (1)读取nightly_Build_Tags内容,格式化显示每个构建标签指示的构建信息目录。 .

    3. 操作系统定时服务

      类名 操作系统定时服务 类英文名 OSScheduler
      成员变量
      变量名 变量说明
      方法
      方法名 参数 执行步骤 方法说明
      excute .
      1. 利用系统当前时间形成日志文件名;
      2. 执行BuildAdmin的excute()方法,并把BuildAdmin的正常输出和错误输出记录到日志文件名中;
      3. 保存日志文件到${logTopDir}指定的目录中。
      启动构建管理服务并记录日志

    4. 构建管理服务

      类名 构建管理服务 类英文名 BuildAdmin
      成员变量
      变量名 变量说明
      cvsroot 保存了cvsroot环境变量
      buildDir 保存了临时存放应用项目源代码的路径
      方法
      方法名 参数 执行步骤 方法说明
      cvs_check_out 应用项目cvs系统中的名字
      1. 利用cvsroot 登录cvs系统;
      2. 执行 cvs co 指令,把参数制定的应用项目源代码取出并放在成员变量 ${buildDir} 指定的目录/项目名字/目录下。
      从cvs中取出项目源代码
      statcvs
      1. module:应用项目cvs系统中的名字;
      2. project_Build_Tag:每个项目的当前构建标签
      (1) 对源代码进行项目度量,并把结果放在${statCVSTopDir}目录下的${project_Build_Tag}目录下。 对源代码进行度量
      execute . 对每一个应用项目执行:
      1. cvs_check_out
      2. statcvs
      3. 执行ProjectBuild的execute()方法
      .

    5. 应用项目构建服务

      类名 构建管理服务 类英文名 BuildAdmin
      成员变量
      变量名 变量说明
      project_Build_Tag 保存了当前正在构建的项目由项目名称和当前系统时间组成的构建标签
      方法
      方法名 参数 执行步骤 方法说明
      test_project .
      1. 为测试而编译连接源代码;
      2. 安装测试版产品;
      3. 测试产品;
      4. 计算测试覆盖率;
      5. 输出测试结果到 ${testTopDir} 指定目录下的 ${project_Build_Tag} 目录下;
      6. 输出测试覆盖率结果到 ${testCoverTopDir} 指定目录下的 ${project_Build_Tag} 目录下;
      .
      dist_project .
      1. 编译连接源代码;
      2. 安装发布版产品到 ${distTopDir} 指定目录下的 ${project_Build_Tag} 目录下。
      .
      execute .
      1. 执行test_project方法
      2. 执行dist_project方法
      3. 把两个方法的日志合并成一个日志文件,命名为 ${project_Build_Tag}.txt ,并把其放在 ${ projectLogTopDir } 指定的目录下。
      .
  3. 执行场景


    1. 构建场景
      • 1.操作系统定时服务类开始执行;
      • 1. 1启动BuildAdmin对象的execute方法
      • 针对每个被管理的项目,执行下列步骤:
      • #begin
      • 1.1.1根据当前系统时间和项目名称生成该项目的构建标签,并记录构建标签
      • 1.1.3以构建标签为参数生成项目的ProjectBuild对象
      • 1.1.2调用cvs_check_out方法,从cvs系统中取出该项目的源代码
      • 1.1.3执行statcvs方法,生成项目度量数据并保存到相应的位置
      • 1.1.4调用该ProjectBuild对象的execute方法,完成项目的测试和安装,并产生相应的构建信息
      • #end
    2. 浏览构建信息场景

      我们已经说过,各个构建信息能实现在browser中的自我展示,所以Nightlybuild对象只需通过某种格式显示各个构建信息的目录,构建信息请求者可以通过这些目录请求各个构建信息。




      1.NightlyBuild对象接到浏览构建信息的请求,通过对自己保存的构建信息目录和构建标签列表组织构建信息目录。





回页首


5、文档书写辅助工具

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

 

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

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

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

1.1 ant 项目构建工具

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

  1. 简述

    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}是个系统属性。

  2. 多个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>
                        

  3. 开发和定义自己的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>
                        

  4. 目标依赖性

    了解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配置文件。

  5. Ant配置脚本的面向对象性

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

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

1.2 junit单元测试

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

  1. 写测试用例

    下面是在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
                        

  2. 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子类。
  3. 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运行这样的测试。

  1. cactus 单元测试原理

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

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

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

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

    1. JUnit 测试运行器调用 YYYTestCase.runTest() 方法。这个方法寻找 beginXXX(WebRequest) 方法,如果找到则执行。 传给 beginXXX(WebRequest) 方法的参数 WebRequest 可用来设置 HTTP头, HTTP 参数,这些参数将被发送到第2步的 Redirector 代理。
    2. YYYTestCase.runTest() 方法打开连向 Redirector 代理的HTTP 连接, beginXXX(WebRequest) 方法设置的HTTP协议参数将被送到代理。
    3. Redirector 代理在服务端作为 YYYTestCase 的代理(其实我们的YYYTestCase被实例化两次,一次在客户端被JUnit 测试运行器实例化,一次在服务器端被代理实例化,客户端实例执行 beginXXX() and endXXX() 方法,服务端实例执行Junit 测试用例的方法 setup(),testXXX(),and teardown()) 。Redirector 代理有下列事情可做:
      • 用java的内省功能创建服务端实例;
      • 设置一些缺省对象;
      • 按照客户端实例的意愿创建session。
    4. 执行Junit 测试用例的方法 setup(),testXXX(),and teardown();
    5. 我们的 testXXX()方法调用服务端代码来进行测试,使用 assertEquals() 方法对测试结果和预期结果进行比较,如果两者相符为测试成功,否则为测试失败;
    6. 如果测试失败,Redirector 代理将捕获testXXX()方法抛出的的异常;
    7. Redirector 代理将异常信息返回给客户端的JUnit 测试运行器,JUnit 测试运行器可以生成测试报告;
    8. 如果没有异常出现, 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方法中调用或模仿这些代码。

  2. 书写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());
                        }
                        }
                        

  3. 配置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报告文档。

  1. 配置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目标。
  2. Clover 分析结果

    下面是Clover 分析结果的图示,读者可以自己看出从这个分析中能得到什么。第一个图是显示一个项目的整体覆盖率情况,第二个图显示了每一个类每行代码的覆盖情况。

1.5 statcvs项目度量工具

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

  1. Ant 中使用cvs

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

    <property name="cvsroot" value="/blog/: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"/>
                        

  2. 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的设置问题。

  1. velocity模版系统工作机制

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

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

  2. 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、文档书写辅助工具

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

本文是实战每晚构建系列的第三篇,利用第二篇文章中叙述的开源技术对第一篇中的分析模型进行设计和实现。

1、构建信息显示系统的设计

这是一个典型的web应用系统,不过非常简单。根据《面向对象的系统分析和设计》所描述的,设计主要对四个部分进行描述:

  • 问题域的细化:考虑将来实现语言的特性和利用某些设计模式,对分析模型进行细化,并作某些权衡。实现对未来系统"如何做事情"的描述。
  • 人机界面设计:考虑和使用者的交互,对信息显示的布局和接收用户指令或数据的行为进行设计。
  • 存储设计:考虑如何保存业务数据,主要考虑现在的存储方式,同时也要考虑未来存储方式的可能变化,即业务对象和存储方式的耦合性问题。
  • 系统接口设计:主要考虑本系统和外在系统之间的合作协议,特别是不要忘了和操作系统或驱动程序的接口。

1.1 分析模型的细化

我们在分析模型中没有考虑实现的问题,现在我们必须细化构建信息显示系统的分析模型,使之达到可实现之地步。

根据前面velocity模版系统地介绍,我们扩充一下显示系统的类图。如下面的"显示系统设计类图"可知,由于Java语言不支持多继承,而且原来分析模型的BuildInfoLog类只有成员变量,我们把这个类改变成Java语言当中的接口来实现,我们把NightlyBuild变成了实现这个接口的一个类,因此同样拥有BuildInfoLog的数据成员。另外NightlyBuild还继承了Java.util.Hashtable类,使得其表现为一个哈西表,这个哈西表以项目的名字为Key,以该项目的构建标签列表为值。根据MVC模式的精神,NightlyBuild类是个业务对象类,应该从信息显示的格式化功能中解放出来,所以NightlyBuild类原来的list_ Buildinfo_table方法放到buildinfo_list.vm模版文件中实现。同时我们增加了一个NightlyBuildServlet类来响应构建信息关心者的访问需求,它是velocity中VelocityServlet类的子类,因此NightlyBuildServlet任MVC模式中的控制角色。

在下面的"显示系统MVC视图"中可以看出,业务对象NightlyBuild对象以nightlyBuild的名字保存在模型context中,当控制NightlybuildServlet用模版文件buildinfo_list.vm来满足构建信息关心者浏览构建信息目录的要求时,buildinfo_list.vm文件格式化保存在context中的NightlyBuild对象的信息并形成视图供浏览者浏览。

1.2 人机界面设计

人机界面设计就是设计浏览者和系统交互的方式,以及系统展示信息的布局。构建信息显示系统是个典型的web应用,浏览者首先在其浏览器中输入构建信息显示系统的URL,系统就会把构建信息目录显示给浏览者,浏览者通过点击构建信息目录页面上的超链就可以访问相应的信息。我们前面说过每个构建信息会在浏览器中自己显示自己,所以现在的任务就是设计构建信息目录显示页面。

下面的图就是构建信息目录显示页面,其中${key}表示某个项目的名字,${nightly_build_tag}为某个项目某次构建的标签。

管理日志超链的url为${logTopDir}/
构建日志超链的url为${projectLogTopDir}/${nightly_build_tag}.txt
度量信息超链的url为${logTopDir}/${nightly_build_tag}
测试记录超链的url为${testTopDir}/${nightly_build_tag}
覆盖率超链的url为${testCoverTopDir}/${nightly_build_tag}
产品超链的url为${distTopDir}/${nightly_build_tag}

每晚构建平台
${key}
构建序列 构建日志 构建日志 度量信息 测试记录 覆盖率 产品
1 管理日志 构建日志 度量信息 测试记录 覆盖率 产品
2            
...            
${key}
构建序列 构建日志 构建日志 度量信息 测试记录 覆盖率 产品
1 管理日志 构建日志 度量信息 测试记录 覆盖率 产品
2            
...            

1.3 存储设计

存储设计主要是指数据库的设计或者文件格式的设计,构建信息显示系统使用文件系统作为存储目标。构建信息显示系统的存储设计主要工作是定义分析模型中BuildInfoDir类也就是设计模型中BuildInfoDir接口的变量的值和格式。

1.3.1 BuildInfoDir接口
类名/接口名 构建信息存放位置接口 类英文名 BuildInfoDir
成员变量
变量名 变量说明 缺省值或值
webAppDir 是应用服务器存放web应用程序的根目录,这个值根据系统安装而定。  
nightlyWebAppName 构建信息显示系统web应用程序的名字,同变量webAppDir一起构成了构建信息显示系统应用程序的根目录。 "nightlybuild"
nightly_Build_Tags 保存所有的构建标签,是构建信息显示系统web应用程序根目录下的一个文件的名字。 "nightBuildLog"
logTopDir 相对于显示系统应用程序的根目录下的一个目录名,保存构建管理服务运行的日志的目录 "adminLogs"
statCVSTopDir 相对于显示系统应用程序的根目录下的一个目录名,保存了所有应用项目的项目度量结果的顶层路径 "statCVSes"
projectLogTopDir 相对于显示系统应用程序的根目录下的一个目录名,保存了所有应用项目构建服务实例运行日志的顶层路径 "projectLogs"
testCoverTopDir 相对于显示系统应用程序的根目录下的一个目录名,保存了所有应用项目的测试覆盖率计算结果的顶层路径 "testCovers"
distTopDir 保存了所有应用项目的发布版的顶层路径,值和变量webAppDir相同  
testTopDir 相对于显示系统应用程序的根目录下的一个目录名,保存了所有应用项目的测试结果的顶层路径 "tests"

下面是nightly_Build_Tags所指的文件格式:

每行包括一个项目构建标签,构建标签的格式为项目名-yyyymmdd_HHMMSS的格式

比如可能的文件内容如下:

nightlybuild-20030312_080100
                    cover-20030312_080100
                    nightlybuild -20030312_080100
                    cover-20030313_080100
                    

这个文件内容表示:两个项目分别为nightlybuild和cover,他们在2003年3月12日和13日的早上8点1分得到了构建。

1.4 系统接口设计

系统接口设计主要描述被设计的系统与外界系统特别是操作系统的接口的设计和描述。构建信息显示系统和构建系统的接口就是存储设计中的BuildInfoDir接口,和操作系统无接口。





回页首


2、构建系统的设计

 
 

发表评论:

    昵称:
    密码:
    主页:
    标题: