系列之一持续集成
既然认为它是好的,就要发挥到极限,这是XP的思想。
持续集成无疑是一种非常好的方法,那么在实际的软件开发过程中就应该把它的好发挥到极限,但就我自己经历过的项目以来,只在一个项目中真正的较好的实现了持续集成,不知道大家的情况是怎么样?持续集成的最出名的代表还是MS的Daily
Build和冒烟测试了。
持续集成的好处
1、集成变得自动化,不依赖人工。
这点好处对于项目是多工程的情况下特别的明显,可以想想,一个项目在多工程的情况下,如果不是 自动集成部署,而需要人手工的话,需要耗费多大的精力,对于单工程的项目来说这点可能不是那么的明显。
2、尽早的发现集成出现的错误。
在不做持续集成的情况下,项目通常也许是在一个迭代的后期才开始将所有人做的东西进行集成,这个时候才暴露出集成的问题,这个时候再去查就已经非常困难了,因为东西已经很多了,采用持续集成可一定程度减少这个问题,因为持续集成保证了每次都只是小部分的集成,出现问题的话也知道范围被锁定在一个小的部分,而且通常可通过单元测试、功能测试以及集成测试来保证在持续集成进行时尽早发现错误,提醒导致持续集成失败的相关人员。
3、不断的发布新的可运行版本。
这点无论是对于开发人员、项目管理人员和客户带来的好处都是很明显的,对于开发人员来说每天可以通过这个版本看到自己所完成的任务,增强任务完成的满足感;对于项目管理人员来说则可以看到项目的开发在按计划的进行;对于客户来说,可以尽早的看到系统以确定和需求的符合性。
持续集成的实现
既然持续集成这么好,那就在项目中引入持续集成吧,怎么样让项目变得可进行持续集成呢?在开源界中对于持续集成有非常多的良好的支持工具,对于持续集成而言,通常是两个部分:
项目自动部署
项目自动部署的工具可选择采用ant或maven,通过将之前项目手动部署的步骤翻译为ant或maven的编译脚本来完成项目的自动部署,当然,这要求对于ant或maven一定程度的熟悉,不过无论是ant还是maven都很容易上手,同时可能会需要对原有工程进行一定的改造,如采用maven一般都需要先把引用的包全部归入一个
仓库中。
一个项目的自动部署脚本有些时候可能很简单,有些时候会很复杂,所有有些自动部署脚本真的是可能需要写上个一天,但这是非常值得的。
在自动部署脚本编写完毕后,就可以通过命令或在IDE中直接部署就可完成整个项目的自动部署的动作。
通常项目自动部署的过程需要有这么几步:
1、清空以往的编译。
2、编译项目工程。
3、运行项目工程中的单元测试。
4、部署工程。
5、如为多工程的项目则其他工程也需要重复上面的1、2、3、4步骤。
6、运行功能测试。
7、生成项目网站(以便查看项目的代码情况、测试运行情况等)(可选)。
在自动部署完成时,即可看到项目的运行版本,同时从项目网站中可了解项目的代码情况、测试的运行情况等。
项目持续集成
项目持续集成的工具可选择采用luntbuild或cruisecontrol(简称cc),两者各有千秋,这里不进行比较。项目持续集成需要根据项目所需要的持续集成的情况来编写相应的持续集成脚本(luntbuild可通过web管理界面完成,cc通过编写脚本完成),通常项目持续集成的脚本会涉及到这些:
1、决定是采用版本管理工具(cvs)感知的方式或每日定时(nightly-build、daily-build)进行持续集成的方式,如决定采用版本管理工具感知的方式,需要确定对于版本管理工具状态获取的频率,如每隔20分钟检查一下是否有更新。
2、持续集成进行,首先是从版本管理工具中获取最新的系统版本,接着就是调用项目自动部署脚本完成项目的自动部署(luntbuild和cc都可直接的调用ant或maven脚本)。
3、合并自动部署后的测试结果到日志中(这点在cc中是要做的,对luntbuild不清楚)。
4、根据自动部署的结果通知相关的人员(email、jabber等等都可以)。
在脚本编写完成后,启动工具项目即可进行持续集成了,可以通过持续集成的网站去查看项目的持续集成情况,其中会包含持续集成执行过程的信息、测试执行的情况、持续集成的统计分析等。
总的来说搭建项目的持续集成环境不会太困难,只要对ant或maven、luntbuild或cc有一定的熟悉,同时明白项目手动部署是如何进行的。
关于工具的使用可以参见几个工具的网站,同时也可满江红.开源参考上的《持续集成实践之cruisecontrol》。
经验总结
从03年开始在自己所经历的项目中就开始实行持续集成,但最终执行的好的只有一个项目,到底是什么原因呢?在这些项目中项目的持续集成环境都是搭建好了的,但就是没执行好,总结下来发现的问题就在于:
1、开发人员没有对持续集成形成足够的重视。
在几个项目中都是因为持续集成在失败后就没人去调好,因为在开发人员本机运行是好的,放入CVS后由于持续集成进行时的环境不一样确实是有可能会造成失败的,但持续集成失败后无人重视,导致之后的持续集成一直就没什么意义。
2、缺乏足够的测试。
导致了项目即使持续集成成功了但可运行版本中仍然是有N多的bug,这说明持续集成是非常需要单元测试、功能测试的支撑的,否则意义将大打折扣。
还有一个是持续集成通常需要耗费很长的时间,web型系统的持续集成通常需要重启web应用服务器;第一个问题倒是可以通过采取增量方式进行持续集成来解决,但由于不是工具直接支持的,所以这块实现还是比较麻烦的;第二个问题就比较麻烦了,反正在我自己经历的项目中这个问题一直没很好的解决,通常是需要在持续集成结束后人工手动的启动web应用服务器(我试着在cc脚本的最后去启动应用服务器,仍然是无效的)。
无论如何,持续集成带来的好处是非常明显的,值得去做,既然是好的,就要把它发挥到极限(对持续集成给予足够的重视、增加测试),那就让我们在项目中实现持续集成吧!
系列之二单元测试
既然测试是好的,那就把它发挥到极限。
测试是好的,这一点无可厚非,几乎做软件的人都是认可的,本篇只是谈谈测试中的单元测试部分,单元测试的目的是为了保证类中的方法是符合设计时的需求的,需求驱动似的类实现,^_^
单元测试的好处
1、保证类对于设计以及需求的符合
在没有单元测试的情况下,其实是很难保证类对于设计以及需求的符合的,很多情况往往会因为开发人员本身的因素将实现代码复杂化,并且编写出很多设计和需求根本不需要的东西。
2、降低调试的复杂性
想想在没有单元测试的情况下,调试通常是集成时才做的,这个时候要通过慢慢的跟踪来查找问题的原因,而在web系统中就更痛苦了,总是要重启,如果不想那么痛苦,就采用单元测试吧。
3、减少集成时出错的机率
单元测试可保证暴露给外部的API的正确性,减少要通过集成才发现错误的现象。
4、保证重构和简单设计的可行
想想,如果没有单元测试,怎么敢对代码做重构呢,如果没有单元测试,简单设计很难通过重构去演变成为将来更好的设计。
单元测试的实现
单元测试的实现采取的方法通常是xUnit,在Java界就是junit了,最重要的仍然不是工具,而是怎么去实现单元测试的方法,测试驱动开发无疑是最佳的编写测试的方法,首先根据设计或需求编写测试,根据测试编写代码,直到测试通过为止。
在代码出现bug时,一定要先把出现bug的情况补充到测试中去,接下来仍然是修改实现代码,直到测试通过。
单元测试编写的原则其实很简单,就是测试一定情况下类的执行是否符合预期。
还是举例来说:
假设需要编写一个根据用户名和密码验证用户的服务,按照TDD我们首先编写单元测试类,我们应该怎么来编写这个单元测试类呢,一般可按照一个这样的步骤:
1、分析类的输入。
这点通常是分析类依赖外部什么类,需要在测试类中提前注入。
以上面的服务来说,通常需要依赖的是用户的Dao类。
2、分析方法的输入造成的输出的影响。
这点通常是分析方法输入的参数对执行结果造成的影响。
以上面的服务来说,输入的参数为用户名和密码,这个时候会有几种情况会出现:
2.1 用户名或密码为null
在这种情况下,假设期待的输出为false
2.2 用户名和密码都为null
在这种情况下,假设期待的输出为false
2.3 输入的用户名和密码在系统中存在
在这种情况下,假设期待的输出为true
2.4 用户名或密码其中一项输入不正确,验证不通过
在这种情况下,假设期待的抛出AuthronizedException
在经过这样的分析后,就可以开始编写测试类了,编写的测试类如下(示例代码而已):
public class UserServiceTest extends TestCase {
private UserDao dao=null;
private UserService service=null;
private User user=null;
public static void main(String[] args) {
junit.textui.TestRunner.run(UserServiceTest.class);
}
protected void setUp() throws Exception {
super.setUp();
dao=new UserDaoImpl();
user=new User();
user.setName("TEST_BLUEDAVY");
user.setPass("JERRY");
dao.save(user);
service=new UserServiceImpl();
service.setDao(dao);
}
protected void tearDown() throws Exception {
super.tearDown();
dao.delete(user);
}
public void testWhenNameAndPassAreNull(){
assertEquals(false,service.login(user.getName(),user.getPass()));
}
public void testWhenNameOrPassIsNull(){
assertEquals(false,service.login(user.getName(),user.getPass()));
}
public void testWhenNameAndPassAreCorrect(){
assertEquals(true,service.login(user.getName(),user.getPass()));
}
public void testWhenNameOrPassIsError(){
try{
service.login(user.getName(),user.getPass());
}
catch(Exception e){
assertEquals(AuthronizedException.class,e.getClass());
}
}
}
在编写完测试类后就可以开始编写实现代码了,实现代码在编写的时候很简单,只要能够保证测试通过就完事,在测试通过后可以开始考虑重构的事,重构仍然只要保证测试通过即可,其实这个时候就可以看到,简单设计就变得可行了,因为可以通过重构来提升设计。
如果将来这段代码出现bug,就把bug中的输入情况也编写为一个测试方法进行测试,开始运行就应该和bug一样出现问题,这时只需去修正实现代码,直到测试通过为止,那么bug也就自然被修正了。
简单的单元测试的编写较为简单,复杂的单元测试则可能需要使用Mock来模拟一些环境,Mock方面的工具有很多,大家可以去参考相关的开源工具的网站。
经验总结
对于单元测试通常很多人都有疑问,执行起来的时候经常是不够彻底,特别是在项目时间紧张的情况下,总是觉得编写测试是一种耽误时间的事,其实编写单元测试会为你节省非常多的时间,想想我们大部分的项目都是在集成、修改bug和维护上消耗了大量的时间,既然单元测试这么好,那么我们就实现单元测试吧。
在单元测试中最重要的注意点就是不要依赖于正常的运行数据,所有的数据都要通过代码模拟出来,在测试完毕后清除,避免造成测试对于运行数据的依赖,同时也避免测试数据对于实际运行系统的影响。
系列之三重构
想改良一个烂设计为好设计吗?想增加或维护代码功能时更加简单吗?重构无疑是其中最好的方法之一,既然它是好的,我们就要把它发挥到极限,把重构发挥到极限的方法就像kent
beck说的,采用两顶帽子的原则,工作中不断的交换帽子,^_^
重构的好处
1、改良设计
设计在一开始不可能做的很完善、很完美,只能是通过在开发的过程不断的去改良和完善,重构就是最好的方法之一,通过重构可将设计快速的改良。
2、增加或维护代码功能时更加的简单
重构进行的原因主要还是因为在增加或维护代码时进行的很困难,这个时候重构原有代码就是为了让增加或维护代码功能变得更为的简单。
重构的实现
重构并不是什么新思想、新技术或者新方法,是一个50年代就已经有N多人融入他们的开发中形成习惯的过程,那么重构到底应该怎么去做呢?在这点上我觉得我没有什么多发言的意义,建议大家参考《重构》一书,书中阐述了很多优秀的编码习惯以及重构进行的场合、方法,重构不象设计模式,重构应该被形成习惯融入到开发中去,重构不是一项独立的任务。
重构依赖于良好的测试体系,如之前讲过的单元测试的贯彻。
Java的开发人员更是可以借助IDE来快速的完成重构的工作。
经验总结
慢慢的重构也变成了我的开发习惯,重构保证了简单设计的可行,同时也保证了软件的质量。
有了重构,我在开发中就可以实行”不要求高质量的实现代码,但要求高质量的测试代码“,高质量的实现代码在任务完成时即可通过重构的技术去进行,就像我以前一篇blog提及过的一样,我在代码实现过程采用的就是:
1、编写能够满足测试的代码。
2、对代码进行重构。
2.1 按照《重构》的一些模式进行
2.2 OO
2.3 设计模式
|