Morning@Weblog

1/28/2005

关于AllTests

Filed under: — site admin @ 9:18 am

一般情况下,在编写Test Case的时候,与Test Case位于相同路径下还应该有一个AllTests.java文件。该文件代表一个Test Suite,包含了当前所有可用的Test Case。在本地开发环境中,除了可以利用build脚本运行Test Case外,一般IDE还会提供具有良好图形界面的Unit Test功能,比如:Eclipse的JUnit插件。此时,如果要运行所有测试,就不用手工逐个执行了,只需要Run “AllTests” as JUnit Test就可以了。

AllTests.java的编写很简单:

public class AllTests extends TestCase {
 public static Test suite()
 {
  TestSuite suite = new TestSuite("All Tests");
  suite.addTest( SomeTest.suite() );
  suite.addTest( AnotherTest.suite() );
  return suite;
 }
}

同时,还需要在每个Test Case中加上一个suite静态方法,比如:

public static Test suite() {
 return new TestSuite(DBTest.class);
}

1/27/2005

关于代码阅读

Filed under: — site admin @ 9:43 am

近日,Java Party的maillist里有人在聊如何阅读与学习源码,当然,这里尤指开源源码。这个话题让我想起早先候捷先生的那篇“上穷碧落下黄泉”,对于这个问题的阐述可谓巨细无余。

在这里,我还想根据自己的切身体会,补充几点:

文档>>

为了做到有的放矢,了解源码之前最好先热热身,翻阅一下相关的文档和资源。在了解了足够背景之后,再行决定是否深入源代也不迟。文档的阅读解决的是“why to”的问题,而源码的阅读则解决的是“how to”的问题,不过实践中,好的文档往往难得一见。

方法>>
源码分析大抵可以分为两种类型:静态分析和动态分析。前者是从静态代码入手,从某个关键点/入口点开始,抽丝剥茧逐层分析;后者则是先将程序跑起来,再结合某个具体场景,采用调试手段,通过分析运行期对象间的协作交互来获得对代码的深层体认。如果自认为天资聪慧,或者是源码简单至极,静态分析也许已经足够。但若是对自己的IQ信心不大,或者源码甚为复杂,那么最好采用情景分析和动态调试。

笔记>>
俗语说“好记性不如烂笔头”,不论是静态分析还是动态调试,笔记是一定要做的,并且必要时候还要画图。在OO系统中,尤其是遇到对象间交互关系复杂的关键部位时,不妨画一下时序图。有UML工具相辅自然是好,不过没有也无妨,一个白板一支水笔足矣,只是这样的真迹要注意保存,辛苦得来的劳动成果也许以后还有用处。不过使用UML工具有一个明显的好处,在对类间的静态关系还不清楚的情况下,可以利用反向工程根据源代码快速生成类图。

工具>>
趁手的工具最好不要少,比如:候sir钟爱的grep。记得我在VC6风行的年代,使用最频繁的就是其IDE中的Find in files功能了,有了这个“简陋”的功能,已经完全可以游刃于MFC的源码之间了。当然,如果有更好用的工具自然好,比如Eclipse,除了强大的调试功能之外,对于源码的交叉索引和快速跳转,将会有助于更快更方便的定位到自己感兴趣的位置。此外,Source Insight也是我很喜欢的一款工具,虽然不能用它来调试程序,但是作为一个轻巧的代码编辑器,其语法高亮显示和交叉索引功能还是很不错的。

其他>>
阅读代码的动机/意图也很重要。不同的动机对阅读代码的效果和最终结果将会有不同的影响,这一点在候捷先生的文章中已经有所提及了。
此外,还有一本专门介绍源码阅读方法的书——《Code Reading》

1/26/2005

Subversion搭建及使用中的问题

Filed under: — site admin @ 11:15 am

网上见到的多是关于Subversion搭建的简单讲解,也许有关于“问题的讨论”更为有价值一些。

在Subversion实际的搭建和使用过程中遇到了几个问题,记录如下:

我在实际搭建时主要参考的是蔡焕麟的《Subversion for Windows 安裝指南》一文,但是在配置完毕重启Apache时总是提示失败(The request operation has failed!),后来发现是少拷贝了一个dll:intl.dll。不过我的操作系统是Windows 2000,而先前在一台XP机器上却没有问题,也许蔡先生的使用环境多半是XP,所以没有遇到这样的问题。

另一个问题与导入有关。我在导入一个原先存于cvs的project时,使用了svs的import命令。但是svn进程在运行了一段时间之后似乎死锁了。后来查找资料发现似乎这个功能一般不常用,一种替代方法是先check out一个空代码库到本地工作目录,然后将要导入的项目添加进来,再用add命令提交。后来我用TortoiseSVN客户端提交成功,但是在提交时发现:除了cpu一直100%运转之外,TortoiseSVN对内存的占用也是一直在上升的(我怀疑这与其实现算法不无关系)。并且,越到后来其提交的速度也越慢。这样的现象在规模较大的project中体现较为明显,比如包含的文件太多(我的project大约有将近3000个文件),目录结构太复杂。我使用的笔记本内存是512M,如果是1G内存的机器,性能方面会好许多。但是,这也足见svn及其相关工具目前而言还不甚稳定。在遇到这样的问题时,可以采取“分批”提交的方式了,svn在面对小规模project的时候还是可以应付自如的。

另:
冰云说svn客户端在解决冲突方面还不如cvs好,不过我还没有实际体会。

subclipse插件的使用也似乎有些问题:在我的试验环境中,当恢复到以前版本时,如果事先做了代码重构,往往会丢失掉重构以前的文件。去看了一下subclipse的网站,似乎自去年2月发布0.9.0版本以后,就一直没有更新过了。有鉴于此,我建议team member在使用svn时,以使用客户端工具(而非subclipse)为宜,因为这样比较可靠一些。此外,subclipse的功能也比较有限。

1/25/2005

Subversion/Apache搭建过程纪录

Filed under: — site admin @ 9:25 am

根据网络资源整理,结合实际搭建情况,在此做一简短小结:
- 分别安装Apache和Subversion;
- 把<subversion_root>/httpd/下的mod_dav_svn.so和mod_authz_svn.so复制到<apache_root>/modules/
- 把<subversion_root>/bin/下的libdb42.dll、libeay32.dll、ssleay32.dll复制到<apache_root>/bin或<apache_root>/modules
- 手工编辑<apache_root>/conf/httpd.conf,取消下面两行的注释:

#LoadModule dav_module modules/mod_dav.so
#LoadModule dav_fs_module modules/mod_dav_fs.so

然后在所有module的最后面新增两行:

LoadModule dav_svn_module modules/mod_dav_svn.so
LoadModule authz_svn_module modules/mod_authz_svn.so

- 手工创建svn的代码库目录<repo_path>
- 运行svnadmin命令,创建代码库:

svnadmin create <repo_path>

- 修改httpd.conf,在末尾增加:

<Location /<repo_url>/<project_name>>
 DAV svn
 SVNPath <repo_path>/<project_name>
</Location>

可以增加多个Location,每个Location对应一个project,它们都位于同一个svn代码库中;
- 也可以利用SVNParentPath来简化上述配置:

<Location /<svn_url>>
 DAV svn
 SVNParentPath <svn_repo>
</Location>

- 重启Apache,通过<svn_url>访问svn代码库,以验证配置是否正确;
- 运行svn的import命令,将代码导入svn代码库:

svn import . <svn_url> -m “…”

其中“.”为当前路径;
- (使用文件方式实现客户端验证)修改httpd.conf,增加用户验证相关配置:

<Location /<repo_url>/<project_name>>
 DAV svn
 SVNPath <repo_path>/<project_name>
 AuthType Basic
 AuthName “Subversion Repository”
 AuthUserFile <userfile_path>
 AuthzSVNAccessFile <accessfile_path>
 AuthGroupFile d:/passwd/groups
</Location>

AuthUserFile指定了用户密码,利用<apache_root>/bin下的htpasswd命令可以创建一个经过加密的用户密码文件:

<apache_root>/bin/htpasswd –c <userfile_path> <user_name>

如果要生成明文文件,可以加上-p参数。
明文的密码文件格式如下:

username:password

AuthGroupFile指定了群组关系,可以在文本文件中手工定义,格式如下:
groupname: username1 username2

AuthzSVNAccessFile指定了详细的权限控制,可以在文本文件中定义,格式类似如下:

[project_name:/]
* =
zhangs = r
lis = rw

除了基于文本文件的简单验证机制外,SVN还支持通过Windows域用户身份、数据库、LDAP等方式来验证。

1/24/2005

DBUnit使用零星小结——DataType

Filed under: — site admin @ 9:15 am

※ DataType

关于数据类型的表达,在DBUnit包中有一个专门的DataType包,里面定义了许多类,分别对应各类数据:
- NumberDataType
- IntegerDataType
- LongDataType
- FloatDataType
- DoubleDataType
- DateDataType
- TimeDataType
- TimestampDataType
- BooleanDataType
- …

它们都派生自抽象类AbstractDataType,而AbstractDataType最终又是派生自另一个抽象类DataType,这看起来有些怪异。DataType里面预定义了很多DataType的静态实例,做法于DataOperation如出一辙:

public static final DataType CHAR = new StringDataType("CHAR", Types.CHAR);
public static final DataType VARCHAR = new StringDataType("VARCHAR", Types.VARCHAR);
public static final DataType CLOB = new ClobDataType();
……

有两个类型比较特殊,ClobDataType是从StringDataType派生的,BlobDataType是从BytesDataType派生的,后二者再从AbstractDataType派生。

众多DataType的职责基本上就是对JDBC ResultSet和PreparedStatement的简单再封装,然后等待DBUnit的上层框架代码调用。关键的方法主要包括:getSqlValue、getSqlType、setSqlValue、typeCast、compare。以NumberDataType为例,其setSqlValue和getSqlValue方法如下:

public Object getSqlValue(int column, ResultSet resultSet)
throws SQLException, TypeCastException
{
BigDecimal value = resultSet.getBigDecimal(column);
if (value == null || resultSet.wasNull())
{
return null;
}
return value;
}
public void setSqlValue(Object value, int column, PreparedStatement statement)
throws SQLException, TypeCastException
{
statement.setBigDecimal(column, (BigDecimal)typeCast(value));
}

然后是一个DataTypeFactory接口和一个缺省实现类DefaultDataTypeFactory,负责根据不同type创建相应DataType实例。

1/21/2005

DBUnit使用零星小结——DatabaseOperation

Filed under: — site admin @ 9:25 am

近日,在使用DBUnit的时候,走马观花的零星阅读了部分实现代码,于是忍不住想做些小结:

※ DatabaseOperation
我们在使用DBUnit的时候,通常会在test case开始运行的时候做数据库记录的准备工作。比如从外部xml文件读入数据,然后做一个干净的插入操作:

IDatabaseConnection dbUnitConn = ...;
...
IDataSet dataSet = new FlatXmlDataSet(inputStream);
try
{
    DatabaseOperation.CLEAN_INSERT.execute(dbUnitConn, dataSet);
}
finally
{
    dbUnitConn.close();
}

类似的数据库操作还有不少,比如:DatabaseOperation.NONE,DatabaseOperation.REFRESH,DatabaseOperation.INSERT ,DatabaseOperation.DELETE,DatabaseOperation.DELETE_ALL。

那么这些预定义数据库操作是在哪里实现的呢?实际上DatabaseOperation在DBUnit中是一个抽象类,而许多具体操作则都是直接或间接从该类派生而来的,并且要实现其excute方法。这里有几个派生类值得注意。

CompositeOperation:内含一个DatabaseOperation[]类型的action数组,实际上是一个action容器,其excute方法会逐个调用每个action的excute。这么做是很tricky的,是compoiste模式的活用。

AbstractBatchOperation:实现了批操作,另有三个类都是从其派生的,它们分别是:InsertOperation、DeleteOperation、UpdateOperation。以下是其核心的执行逻辑:

// For each table
while (tableIterator.next())
{
    ITable table = tableIterator.getTable();
    // For each row
    int start = _reverseRowOrder ? table.getRowCount() - 1 : 0;
    int increment = _reverseRowOrder ? -1 : 1;
    for (int i = start; ; i = i + increment)
    {
        int row = i;
        // for each column
        Column[] columns = ... // get columns;
        for (int j = 0; j < columns.length; j++)
        {
            Column column = columns[j];
            statement.addValue(table.getValue(row,
                    column.getColumnName()), column.getDataType());
            statement.addBatch();
        }
        statement.executeBatch();
        statement.clearBatch();
    }
}

DummyOperation:对应DatabaseOperation.NONE,实际上位于DatabaseOperation内部的一个静态私有类,execute方法是空的。

最后,DatabaseOperation还定义了一组static实例:

public static final DatabaseOperation NONE = new DummyOperation();
public static final DatabaseOperation UPDATE = new UpdateOperation();
public static final DatabaseOperation INSERT = new InsertOperation();
public static final DatabaseOperation REFRESH = new RefreshOperation();
public static final DELETE = new DeleteOperation();
public static final DatabaseOperation DELETE_ALL = new DeleteAllOperation();
public static final DatabaseOperation TRUNCATE_TABLE = new TruncateTableOperation();
public static final DatabaseOperation CLEAN_INSERT = new CompositeOperation(DELETE_ALL, INSERT);

这里的CLEAN_INSERT就是用的CompositeOperation,然后分别包装了DeleteAllOperation和InsertOperation实例。

在这里,DatabaseOperation还同时扮演了Factory的角色。不过这样就在基类中暴露了子类信息,按理说是一种不良设计。但是针对这个具体场合,类结构本身就是比较固定的,也很容易维护,也就不用再多此一举的把问题复杂化了。

1/20/2005

自动测试 vs. 人工测试

Filed under: — site admin @ 11:08 am

来看一个经典的例子,通常的登录测试验证包含如下几个步骤:
- 获取登录页面
- 验证页面标题是否为登录页面
- 填写用户名和密码
- 点击确定按钮
- 验证页面标题是否为登录后的首页

也许这样的简单例子不足以让你觉得有必要采用自动测试技术,但是请考虑以下若干变体:
- 如果试图不通过登录而访问首页会怎样?
- 登录失败页面将重新导向登录界面
- 对于其他页面,是否保证了没有经过有效登录将不会显示。

虽然通常情况下,人工测试更为灵活,单次执行的代价也不高,并且相对于自动测试一开始就要编写一堆测试脚本而言,起步很低。但是,在测试需要反复多次执行的时候,人工测试就体现出其代价高昂与低可靠性的缺点了。以上面的登录验证为例,对于手工测试者而言,他需要记住所有这些场景并逐一验证,这是繁琐而易错的。

鉴于自动测试在这一点上的优势,如果不用花费太大代价就可以实现自动化测试,那么应该尽可能实现测试的自动化,因为它是有效保证代码质量的关键。

1/19/2005

功能测试 vs. 单元测试

Filed under: — site admin @ 9:47 am

持续集成所带给我的关键启示之一就是自动化测试的重要性。说到测试,一般包含功能测试和单元测试。对于两者的区别,以及何种情况下使用何种手段,这里到是有一个极为典型的例子,它说明:功能测试并不能完全取代单元测试,两者应该是齐头并进的。

场景描述:
- 一个web应用程序中要显示html表格,其数据来自于后台数据库;
- 表格的最大行数是20,一旦超过20行将会分页显示,如果没有任何数据,则会显示一条提示信息;

为此我们可以分几种情况来测试:
- 没有数据
- 1条数据
- 20条数据
- 21条数据
- 40条数据
- 41条数据

一个直观而笨拙的做法是通过操纵数据库来进行测试。但是,这么做不仅低效,同时还潜藏危险:如果基于同一数据库的两个test case并发执行会怎样?它们有可能彼此破坏测试数据。如果测试异常终止,数据库的状态是否能够回滚?

由此可见,整个过程用功能测试来完成是十分困难的。但是,如果用单元测试就可以轻松搞定,因为它可以脱离数据库而进行测试。

在本例中,我们可以利用单元测试来验证html表格的具体显示逻辑是否正确,然后再借助功能测试来验证表格显示逻辑是否被成功调用。

1/18/2005

开发基于J2EE架构的若干原则

Filed under: — site admin @ 11:43 am

有关开发尽可能最简洁的J2EE架构的若干原则
- 没有充足的理由就不要使用分布式架构
- 没有充足的理由就不要使用EJB
- 不要自己动手开发复杂的底层设施,使用现成方案,比如开源方案
- 现在不要假设将来的事情,否则就会引发额外的开销和复杂度
- 花时间去领会你的业务需求,决定架构的是业务需求,而非技术平台
- 在提出复杂架构之前,用性能以及其他度量形式来为此寻找证据

(摘自J2EE Development Without EJB)

1/17/2005

当DBUnit遇到了Oracle Clob

Filed under: — site admin @ 3:03 pm

在使用DBUnit实现数据库数据初始化的时候遇到了一个问题。在向Oracle的数据库表空间中插入记录时由于存在Clob字段,而报出类似如下的SQLException:数据大小超出此类型的最大值:***。

查看DBUnit的源代码之后发现,这是由于DBUnit在做数据库插入时,缺省使用的是其org.dbunit.dataset.datatype包中的ClobDataType,而该类使用的是“一般性”方法:调用PreparedStatement的setObject,并给定SQL类型为LONGVARCHAR。但是这样的方法在遇到Oracle的Clob时是行不通的。

后来发现DBUnit在其扩展包org.dbunit.ext.oracle中提供了一组类,以支持Oracle的Clob/Blob类型。对于Clob类型的处理,其基本思路是:
- 利用oracle.sql.CLOB的createTemporary方法先创建一个临时Clob对象
- 以可读写模式打开临时对象,并利用getCharacterOutputStream获取Writer实例
- 把值写入临时对象中
- 关闭Writer实例和临时对象
- 利用PreparedStatement的setObject将临时对象“塞”入数据库
使用时需要将原先的DatabaseConnection替换成OracleConnection。并且由于使用了Java反射机制,在编译test case时,并不需要Oracle的jdbc driver。

可我在实际使用时却又遇到了新的问题,test case在运行期间报出了createTemporary方法的NoSuchMethodException。去Oracle的CLOB类中一查,发现确实没有此方法,于是恍然大悟:由于我用的是Oracle 8的thin driver,应该是没有createTemporary的。这就只能使用传统的处理Clob数据的方法了:
- 插入一条Clob数据为空值的记录(用empty_clob函数)
- 在将auto commit设置为false之后,通过select for update选出插入的记录
- 为选出的结果集中的clob设置取值,然后update

原本想修改DBUnit的实现代码,不过后来发现这种“三步走”的方法与DBUnit外围的框架代码是向背的,希望通过效仿org.dbunit.ext.oracle的扩展方法来达到同样的目的是行不通的,根本原因在于OracleClobDataType类中可做的扩展及其有限,包括插入数据在内的大部分事情都由外围框架代劳了(并且是一个批操作)。因此,最后决定放弃这种无谓的努力,要么换成Oracle 9i,或者使用其他的数据库,比如mysql。

1/16/2005

CVS的使用,多个module还是一个module?

Filed under: — site admin @ 8:35 pm

针对软件的多个模块,有一种选择是在配置管理系统中为其各自建立一个项目,在CVS中就是为其建立不同的module。这样一来就会带来一个问题:如果模块之间彼此依赖,那么为了取得不同的软件模块,开发者就不得不记住哪个模块的哪个版本使用了其他模块的哪些版本。

尽管有些场合下,你不得不将源代码分项目组织,但是这种情况出现的几率要比你想象的小得多。实际上完全可以从一棵源代码树上分出多个模块来,上面那些问题完全可以通过构建脚本来加以解决(比如:ant),而不是去改变软件的存储结构。虽然你可以使用诸如anthill、cruisecontrol这样的持续集成平台来管理项目间的依赖,但是前提是你在自己的开发团队中采用了持续集成手段,并且这种做法依然是值得商榷的。我能想到的最简单的理由是,用一个项目组织也照样运转的好好的,何必多此一举呢?

1/15/2005

持续集成平台经验教训总结(4)

Filed under: — site admin @ 7:37 pm

(关于如何编写测试用例,也许需要专门的单独篇幅来讨论,这里只是阐述其重要性)

- 自动化测试在持续集成中的重要性

如果只是软件的自动编译,那么很多运行期错误还依然潜藏着。持续集成的排错能力很大程度上取决于测试技术,只有测试覆盖足够大,才能找出足够多的bug(当然不是全部bug)。所以一定要强调测试环节,这从一个侧面,也体现了测试的重要性。

测试包含单元测试和功能测试,应该尽可能提高自动化程度,这里尤指功能测试。针对web应用,可以利用Canoo webtest和HttpUnit等工具,实现自动的可重复的测试。

对于测试用例的编写,通过反馈来提高测试的质量(XP的一项核心价值)很具有指导意义:对于在单元测试中逃脱的bug,应当在修正错误的时候,同时补上相应的测试用例,以确保单元测试集不会再次把错误放过。并且,这个测试应该引导你去考虑编写更多的测试,以加强和完善测试集。

总之,运行更详尽的测试集可以大大提高持续集成的价值。同时,作为一种积累,一套完善的测试集,以及自动化的测试过程,从产品发展的长远角度来讲也是很有必要的。

1/14/2005

持续集成平台经验教训总结(3)

Filed under: — site admin @ 2:21 pm

- 关于自动构建的时间间隔

首先说说每日构建。两次构建的间隔是一天,由于一天里的代码变动不会很多,因此发现问题就会很快,问题的解决也会相对容易。这可以看作是分散了“捉虫”的总体代价,减少了团队成员在集成阶段“捉虫”所耗费的体能和时间。同时,一旦遇到构建失败,你也可以很容易的快速恢复到上一次成功构建的版本。微软在上千万行代码的项目中仍然坚持每日构建。

经验数据显示,集成的工作量是与两次集成间隔时间的平方成正比的。在XP社群中,每日构建被认为是最低要求。实际上,如果以往的集成力度不够的话,在版本临近发布的阶段,小于一天的高频度集成完全是可以接受的,如果有足够多的测试保证的话,如此频度的自动构建可以有助于我们在频繁更动和集成的同时,随时发现问题。这样我们就不用害怕要为随时可能到访的客户做软件演示了,也不用专门准备一台演示用服务器,装上一个主观认为没有问题的专为演示目的的“过时”版本了。

- 性能是问题吗?

对于每日构建而言,性能不是问题。既然平台已经成功搭建,所有程序都是跑在持续集成服务器上的,因而并不会影响每个团队成员各自的本地开发。

对于更短时间的集成频度,比如代码库一有更新就自动构建,就需要一台性能优良的专用服务器了。试想,当开发者将改动结果提交到代码库后,为了看到最近一次集成是否成功需要等待足足半个小时,那是无法忍受的。

有个问题值得注意,用于持续集成服务器自动执行的构建脚本,同时也用于本地开发。因而,在本地开发环境中,除非干净的构建(clean build),建议平时尽量采用增量构建,因为此类构建比较快速,并不对开发人员的开发进度构成太大影响。并且,脚本自身也应该提供足够的灵活度,可以有选择的编译某个部分,或者有选择的运行某些测试,以进一步缩短构建过程。当然,必要的干净构建依然是不能少的,否则某些bug就很容易在本地开发时漏掉。但是不管怎样,此类bug不会逃过持续集成服务器的眼睛。

1/13/2005

持续集成平台经验教训总结(2)

Filed under: — site admin @ 10:46 am

- 持续集成可以做什么?

完整的持续集成过程,应该是一个完全自动化的,并且是可重复的软件构建过程:自动从代码库中取得当前最新的软件快照(snapshot),然后编译、测试、打包、部署、运行。在这里要指明的是,代码库所包含的不单只是代码,还包括构建脚本、属性文件、安装脚本、以及在一台干净的机器上完成一次完整的构建过程所需要的一切素材。

这里的关键在于自动化,即:要让所有的事情都尽可能自动完成——一条命令就可以完成整个系统的构建,完成一整套测试。

- 只实现自动编译的持续集成平台意义不大

如果持续集成只实现了自动编译,那还是远远不够的,要实现这一功能,任何一个开发者只要随时在本地机器上运行一下相应的构建脚本就可以达到同样的目的。

持续集成只有加上足够的自动化测试,相关报告的自动生成,以及软件的自动部署和发布,才能愈加体现出其价值所在。否则,只是让平台编译一下整个软件以验证是否存在编译错误,其意义并不是很大。于是,我认为花很大力气去搭建一个这样的持续集成服务器,完全是没有必要的。

- 持续集成本身不能保证代码质量

有了持续集成并不能保证软件过程的良性改进,构建过程显然也无法保证当前最新的代码是可运行版本。当构建失败时给每个开发者发一封mail并不是最终的目的,相关负责人需要及时纠正错误,并且这最好能得到相应制度的保证。

1/12/2005

持续集成平台经验教训总结(1)

Filed under: — site admin @ 11:00 pm

- 持续集成平台存在意义是什么?

一般普遍地认为,持续集成平台的主要作用在于小版本的持续发布,以及在小版本成功发布的基础之上,团队成员对项目进度把握能力的增强和对预期目标信心的增强。是的,如果是单枪匹马,完全犯不着用持续集成平台,只有当多人协作开发的时候,持续集成才能发挥其应有的作用。因为,它可以发现多人代码集成过程当中的隐患,并且第一时间暴露问题。

- 没有持续集成平台是否可以呢?

当然,在没有持续集成的年代,人们照样开发软件,并且即使是现在,也依然有人在这么做。只是,这样是有风险和代价的,并且,即使有了持续集成平台,没有用好,同样回避不了代价和风险。临近发布阶段的加班加点难道真的是天经地义的吗?在为客户演示时提心吊胆难道真的是亘古不变的吗?我想未必,如果有好的计划管理和过程控制,就不会前松后紧,正如在第一段中所提到的,持续集成平台无疑是一个很好的辅助工具。既然采纳了持续集成平台,那么就应该协力把它尽量做好,而不是半途而废。

- 不经常check in的持续集成是没有意义的

这个结论很容易推得。试想如果每隔几星期才check in一次,那么每日构建在最近一次成功构建之后,要经过一段相当漫长的时间才有可能暴露问题。并且,一旦构建失败,往往是积重难返,因为它是累积了数周以来的全部错误。而剩下的时间里,平台都在报告着一个不痛不痒却又是隐藏危险的Success信号。

因此,对于每日构建而言,实际上隐含要求了每个开发者至少每天要check in一次代码。换言之,持续集成给开发者提出了隐性的开发习惯上的要求,进而一定程度上很自然的约束和规范了开发过程。

1/10/2005

关于CVS的使用,一点小结

Filed under: — site admin @ 4:01 pm

- 强烈建议不要从根部毫不犹豫的commit,因为这样会把很多临时的、私人的目录和文件提交到CVS里面,本着“对自己代码负责,并且尊重同事劳动成果”的原则,建议只对自己每次修改的部分(目录或文件)进行commit;
- 对某些不适合用binary方式提交,而eclipse的cvs插件缺省又会以binary方式提交的文件(比如jsp、tld文件),请在Preferences->Team->File Content中做好设置;
- 一旦发现有不应该提交成binary格式的文件,可以通过在文件上右键点选Team->Change ASCII/Binary Property…来“补救”,建议选择“ASCII with keyword substitution”方式,然后选择“Include files that are already shared in the repository”;
- 建议在commit之前最好先作一下同步,这样一方面可以明确自己要提交的内容,另一方面也可以事先知道存在的冲突。具体方法是,在要提交的目录或文件上右键点选“Team->Synchronize with Repository…”
- 如果因为某此无意间的误操作导致冲突发生,对于带有类似如下片段的文本文件(包括源代码文件),请使用手工编辑的方式解决冲突,然后再次提交:

< <<<<<< .mine
Salami
Mortadella
Prosciutto
=======
Sauerkraut
Grilled Chicken
>>>>>>> .r2

- 在开始新的任务之前,应该首先与CVS同步。也就是说,应该首先更新本地机器上的源代码。这是一种良好的开发习惯,在旧有的代码基础上编写代码,只会带来麻烦和混乱;
- CVS的代码库作为统一的代码存放地,保证了软件代码的一致性,绝对不能走回头路;

Powered by WordPress