2007年09月30日

报表测试根据项目的定义有大有小,有时只是作为软件的一个部分进行测试,有时整个项目都是测试各种报表.但不论如何,报表的作用始终都是将系统中已经存在的数据根据用户的设置计算加工/整理汇总/最终以清晰的格式展示给用户,以便用户进一步做数据分析或统计.

  软件中的报表实现一般分为定义报表的所需数据(一般可以通过选择或手工输入条件来缩小数据范围)和定义报表格式两个部分.报表格式除了如国家各行业标准中规定的报表使用固定格式外,大多是根据企业或用户的需要定制报表.

  所以,做报表测试时要注意以下方面:

  1.数据的正确

  用户使用报表就是期望通过一个简单方便的平台能快速的查找到他所需要的数据.所以在测试报表时首先就要检查报表中的数据是不是用户需要的数据,如果没有加工的数据,是否保持了原貌; 加工过的数据查看加工的结构是否和手工加工的结果一致.简言之,需要测试以下内容.

   数据的来源:来源于哪张表,哪个字段,数据库中的数值与界面数据的对应.如数据库中性别的数据可能是0或1,但界面显示为男或女,这个对应关系是否正确.
   数据的范围:是否只显示了报表设置的对应范围;特别要注意边界数据,要清楚报表的需求,是否需要过滤掉被选择的数据.如时间选择为2006-9-27~2007-9-27,那么是否应该包含9-27这天.
   数据的对应关系:数据库中的字段是否与报表中的信息对应
   数据的格式:小数位,千位符,四舍五入等是否与报表设置一致;单位或税率转换是否正确;组合显示的数据是否合理
   数据的排序:排序方式是否与报表设置一致(如果没有设置,是否有一个清晰的默认排序方式,如按字母或数字排序)
   流水号:如报表有使用流水号,流水号的生成和格式是否正确.取消操作是否会生成流水号.
   明细与合计的一致性:各部分明细或小节是否与最后总和一致
   其他

  测试这一部分内容需要对业务逻辑相当熟悉,对数据库的设计也要非常了解.必要时可以通过自己写查询语句查看数据.

  有些报表的条件有多有少,但测试方法都是一样.根据条件通过等价类划分和排列组合设置各种条件组合.千万不要盲目的测试,否则会导致该测的没测,多余的测试做了一堆..一般来说有类别划分的(一般界面表现为下拉框),每个类别都要测试到,如性别中的男,女都要测试.输入的可以用等价类来划分要测试的数据.

  2. 格式的正确

  数据验证正确后,就需要看看报表的输出格式是否符合要求.可以从以下几方面来检查.

   报表的整体风格:报表是否符合规定的或用户设置的格式
   报表标题:报表的标题是否是正确的报表名称;如报表中有嵌入的数据(会跟随用户的选择而变化的).需要检查数据是否正确,如XX企业9月份财务报表,这个9月就是用户选择的; 或者XX公司2006-9-27~2007-9-27的网站访问量,这个时间段也是用户选择的.
   公司的一些标志:如logo,名称,地址之类的是否正确
   报表的页首与页尾:是否采用了一致的规则.
   分页:当输出的内容多时,分页是否正确.翻页功能是否正确
   友好性:数据或图表是否清晰,一目了然,数据的展示符合用户的习惯;需要特别提醒的数据(如合计,异常数据)是否突出显示;复杂算法处,用户不明白或容易混淆处是否有注释;一些默认的格式是否让人感觉舒服,如对齐,边界,间隔等

  3. 权限的控制

  对于有权限控制的系统,报表当然也应该和用户所具有的权限相一致.需要从两方面校验权限的控制.

   报表的条件定义:在条件选择区域,有些下拉框中应该不能显示用户权限范围外的数据.如普通文员在使用报表时,报表名称下拉框中是不可以显示管理者才能查看的报表的.有些以输入的文本框有级别的划分时,都应该要测试输入超越权限的数据的相应.
   注意这里一定要测试每个条目.
   报表内容:报表中的内容不能显示用户本没有权限查看的数据.

  4.报表的输出

  报表在电脑上生成后,并不是报表的结束.报表一般都需要打印出来他用,如开会或者提交审批之类.所以报表的打印功能也是非常重要的.测试主要分成三部分:

   l 打印设置
   l 打印预览
   l 实际打印效果

  除了打印之外,用户有可能需要导出报表做进一步的分析或用于和其他报表的比较.所以也应该提供导出报表的功能.一般可以导出为CSV,Excel,pdf,html,xml格式.看公司需要了.这里主要要检查导出的报表默认属性是否为读写,然后导出的内容是否正确,与生成的报表相一致.

  5.报表与报表之间的关系

  有些报表都使用了相同的数据,只不过针对不同的需要做了不同的处理.所以报表与类似报表之间要做些测试,看看数据是否一致.

  6.报表的性能

  用户在设置好条件后都希望不要等待报表太长时间,当然有时数据量大时等待时间长些也是合理的.但是在做报表的开发时或测试人员可以提出一些意思来提高报表的性能.

   l 报表的条件设置区域应该设置默认值以避免用户不输入任何条件直接生成报表所造成的长时间等待.例如开始和结束时间可以默认为当前的一个月,一些输入文本框可以根据用户的身份默认一个数值.
   l 生成报表时用类似进度条表现进度,避免用户盲目的等待
   l 生成报表的语句尽量采用最优的查询语句,多调试几遍,查看语句的性能.

  7.报表控件的独特性

  一般公司会用专门的报表控件来生成报表,例如MS的Report service, Crystal报表等.所以最好先了解一般的报表生成流程和这类报表控件的特点,这样在测试时就可以有的放矢,而不是盲目的比较.

2007年09月29日

Loadrunner是一款负载测试工具,它有三个核心组件分别是Virtual User Generator、Controller、Analysis。Virtual User Generator 可以通过录制脚本准确的记录下来用户的每一步操作并且可以进行集合点设置、事务设置、参数化等操作从而为在Controller中执行特定的场景做准备。Controller顾名思义,它可以控制脚本的执行,通过把脚本放置在一个特定的场景中,模拟一批真实用户的操作过程,这些模拟的真实用户就叫做虚拟用户。通过这些虚拟用户可以对系统进行负载测试。Analysis应该是测试人员极为关注的一个组件,通过Controller执行完某一个场景之后,Analysis可以自动生成测试结果并通过图形的形式显示出来,测试人员只有借助这些图表才能准确分析出系统的瓶颈并且确定性能是否达到要求。

下面介绍一下如何进行集合点、检查点以及参数化的设置:

对于集合点、检查点的设置有两种方法,一种是在录制完脚本以后,手工在脚本中添加相关的关键字例如lr_start_transaction等,这种方法对脚本语言的理解能力要求较高。另一种是直接在录制的过程中添加集合点、检查点,这样lr就会自动把集合点、检查点的关键字添加到脚本中。

事务:就是用户某一步或者某几步操作的集合。当我们需要通过某一步或是某几步操作从而衡量服务器的性能的时候,这时我们就把这些操作设置成一个事务,当事务开始执行的时候lr就开始计时当事务运行结束计时停止,执行事务的时间会在在最后的结果中显示出来。

实例:登录sina网站,把点击“天气”设置成一个事务,衡量服务器处理处理该事务的性能。

1,点击红色的录制按钮,输入URL开始录制。弹出sina的首页,点击<!–[if !vml]–><!–[endif]–>设置事物的开始位置,这时弹出事务开始对话框要求输入事务的名称,一般来讲我们都会把事务名称命名为容理解的名字,此处我们命名为“天气”

点击OK完成事务的开始点设置。

2,在sina页面上点击“天气”的连接,出现天气页面

3,点击<!–[if !vml]–><!–[endif]–>设置事务的结束点,这时弹出事务结束对话框

lr根据匹配原则已经自动把事务名字一栏填入“天气”,我们只需要选择事务的状态。状态有三种LR_AUTO、 LR_PASS、 LR_FAIL、 LR_STOP,

LR_AUTO:事物的状态被自动设置,如果事务执行成功,状态设置为PASS,如果执行失败,状态设置为FAIL,如果由于异常中断,状态被设置成STOP.

LR_PASS:事务如果执行成功,代码的返回状态就是PASS。

LR_FAIL:事务如果执行失败,代码的返回状态就是FAIL。

一般我们选择LR_AUTO。 那么我们会有疑问什么时候我们选择PASS或者是FAIL呢?

Lr的帮助文档中有一条例子,可以很好的帮助我们理解

lr.start_transaction("GetStocks");
try {

String stocks[];
stocks = orStockServer1.getStockList();
if (stocks.length == 0)

throw new Exception("No stocks returned/available");
lr.end_transaction("GetStocks", lr.PASS);
}

catch (Exception e1) {

lr.end_transaction("GetStocks", lr.FAIL);

lr.message(" An exception occurred : " e1.toString() );

}

代码说明:这时一个得到stock list的例子,程序中设置了异常检查来确保getStockList()方法返回非零的长度。

 

同时我也进行了如下的脚本修改

………………

lr_start_transaction("天气");

lr_think_time( 3 );

web_add_cookie("mysinal=ai_erica; DOMAIN=weather.news.sina.com.cn");

web_add_cookie("SINAGLOBAL=221.219.31.58.924471172571904604; DOMAIN=weather.news.sina.com.cn");

………………

lr_end_transaction("天气", LR_FAIL);

在最后我把该事物的结束状态设置为FAIL,然后运行该脚本,其实该事物的运行是没有任何错误的,完全可以运行成功,只是在最后我把事务的状态手工设置为FAIL。当脚本执行完后,查看Ececution Log看到这样一条log语句:

Action.c(297): Notify: Transaction "天气" ended with "Fail" status (Duration: 5.1436).

那么这样做的意义是什么呢?为什么要设置事务结束状态呢?原因就是在Analysis中生成结果图表的时候我们就能看到这个名为“天气”的事务执行是失败的。如果语句是这样:

Action.c(297): Notify: Transaction "天气" ended with "Pass" status (Duration: 5.1436).

Analysis中生成结果图表的时候我们就能看到这个名为“天气”的事务执行是成功的。

设置事务结束状态的用途就在这里。试想Lr为什么能自动生成结果图表?无非就是Analysis通过一些定义好的API获取执行脚本过程中的返回值,从而显示出事务执行的正确还是错误,或是显示出响应时间等信息,然后调用GUI使我们很直观的看到测试结果。

2007年09月28日

运行前提
1. Windows2000 Server 服务器上已经安装Rational ClearQuest 2002.05 版。
2.Windows2000 Server 服务器上已经安装 SQL Server 2000
3.Windows2000 Server SP3

一、在SQL Server 上建立空的
a.先在SQL Server 上建立一个空的数据库,建库时请注意给ClearQuest 的主数据库(Schema Repository) 数据文件分配至少50M的空间。如图一所示:
b.为ClearQuest 主数据库建立专门的用户。注意:不要使用SA作为ClearQuest数据库的Owner,这是因为当你将来要进行更新或迁移ClearQuest主数据库时,ClearQuest将
会向SQLServer请求一个空的数据库。可是,如果以SA用户登录ClearQuest主数据库时,因为SA可以访问到
系统表,故在迁移或更新ClearQuest主数据库时将不能够继续进行。建立Clear
Quest专门的登录用户步骤可见图二和图三. ClearQuest用户必须使用SQL Server的身份验证,同时将默认的数据库设置为ClearQuest.
二、使用 Maintenance Tool 建立ClearQuest的主数据库
运行ClearQuest Maintenance Tool , 从菜单上选择“Connection -> New”来建立一个ClearQuest的主数据库(schema repository),即保存你
定义的各种方案。如
接下来我们需要在SQLServer 2000服务器上建立ClearQuest服务器。当然如果你选择ACCESS 数据库直接按回车即可。当你在Vendor: 中选择SQLServer 后(见图五),将
会出现有关与SQLServer 服务器连接的信息设置。具体设置如图六:可以通过右键项来改变CQ主数据库的命名,我们可以将其命名为:MyTest.

上次有个网友问我:“HTTP, 当使用Read-Only User我怎么也连接不到数据库中”。当时我试了多种方法也仔细查过相关资料,只能通过其DB Owner 才可连通。 如果使用只有[读]权限的用户
会失败的,不知道其它人是如何解决此问题的?有人知道有劳通知大家。 :)不过在使用过程中没有较大的影响,如果是在2002.05以前的版本时,使用时会存在一些安全漏洞,因为必竟 DB Owner的权限过大
些。呵呵,事在人为嘛。接下来CQ Maintenance Tool 将会显示建立CQ主数据库的过程,按提示点击确定即可。到此为止CQ的主数据库即大功告成了。接下来我们将进行如何在ClearQuest
Designer 中建立各种方案(Schema) 。
三、使用CQ Designer 建立各种方案(Schema)
当你运行ClearQuest Designer 时,会出现请你选择使用哪个 CQ 主数据库,我们在这里选择上面建立的: MyTest. 在这里请注意,我们说明界面均是CQ 2002.05版,以前的版
本界面不是这样的。如图七:
第一次运行 ClearQuest Designer 时,请使用用户为:Admin 密码为:空,登陆进入到ClearQuest Designer中.此处的用户不同于主数据库的用户. Designer中的
用户是用来在使用你设计的方案时所需的用户,由 Designer 自已的用户管理器创建.并为其分配相关的数据库访问权限. 当你在Designer 中建立数据库时,前提是你必需在 SQL Server 上
建立好一个空的数据库,同时为此库建立自已独立的DB Owner. 然后才可运行 Designer 进行建立方案.
当进入CQ Designer 后,首先弹出的窗体为CQ中向你提供的八个应用方案.你可以根据自已的应用情况选择合适的方案,当然可以自已完全定制一个方案,关键是看你对CQ的了解程度。我建议先自已学习它提供
的方案,然后自已动手定制一个完全符合自已的应用方案。因为CQ中提供的方案一般与Rational的其它产品结合较为紧密,许多功能我们暂时用不上,没有必要花很大的力气了解它,路要一步步走嘛。在此我们以CQ
提供的”Defect Tracking” 方案为例,建立一个自已的方案步骤。如图八:
进入CQ Designer 后,先取消图八的窗体。 然后在CQ Designer 的主菜单上选择”Database à New Database” 项。将出现如图九所示窗体,即为建立方案库的第一步。该
窗体中的 Logical Database Name 为CQ Designer 管理各种方案而使用的一种逻辑库,在CQ Designer 中使用这些逻辑库来进行方案的删除,恢复删除和更新. 这里的逻辑
库并不是你在SQLServer建立的表。
点击 [下一步]后,进入建立方案库的第二步;将出现连接你已经在SQLServer 建立的空表的信息 如图十,其中需注意的有以下两点:
1.连接数据库的用户必须是该空表的DB Owner ,其它具有读/写的用户仍连接不成功。原因同上面我说的,待查。 :(
2.在最下的请选择 Production Database ,它代表此方案用于实际应用,而并非专为测试方案 —- Test Database 使用。有关测试方案库我们会在以后再讲。
在图十上点击[下一步]将进入建立方案库的第三步, 即为方案定制超时设置。 一般情况下可以为默认值。再点击 [下一步] 为建立方案库最后一步,在CQ提供的方案模板中选择我们要创建的 “Defect T
racking ”方案。如图十一所示:
最后点击 [完成]按钮,拿一杯热茶等着吧, 如果一切顺利将会出现”Database was created successfully”对话框。恭喜你成功了!
想进一步验证,可以通过ClearQuest 客户端来进行,动行ClearQuset, 在其出现的首个对话框中选择你刚才建立的方案,使用管理员进入后便可进行其应用了。
Rational ClearQuest 功能很强大,以后有机会我们大家多交流,写出更多更好的使用经验点滴,希望我这陋文能起到抛砖引玉的作用。同时也希望能与大家交流使用经验,我联系Mail: hans_cheng@hotmail.com.
为了安全,提醒您请及时备份您的CQ主数据库与各方案数据库.数据

2007年09月27日
Rational系列产品大概的介绍 Rational Application Developer for WebSphere Software
用于架构和建模、模型驱动开发、组件、组件测试、运行时分析活动的工具

Rational Professional Bundle
提供企业级桌面工具,以便设计、构建和测试J2EE/门户/面向服务的应用程序

Rational Rose Developer for UNIX
提供行业领先的模型驱动开发工具。

Rational Rose Technical Developer
一个模型驱动开发解决方案,针对Java、C、C++自动进行从设计到代码的转换。

Rational Rose XDE Developer for Java
为基于J2EE 的系统提供完整的可视化设计和开发环境。

Rational Rose XDE Developer for Visual Studio
基于.NET 的系统提供完整的可视化设计和开发环境。

Rational Rose XDE Developer Plus
为基于J2EE 和基于.NET 的系统提供可视化设计和开发环境。

Rational Software Architect
利用 UML 为模型驱动开发提供整合设计和开发支持。
Rational Software Modeler
支持 UML 可视化建模/设计,从不同的视图编制系统文档。
Rational Suite DevelopmentStudio for UNIX
合并屡获殊荣的开发工具,帮助人们更快速地构建更好的软件
Rational Suite for Technical Developers
支持诸如实时和嵌入式技术应用程序的可视化开发。
Rational Web Developer for WebSphere Software
简化和加速了 Web、Web 服务和 Java 开发。

过程项目管理

Rational Portfolio Manager
协调优先级、项目和人员。

Rational ProjectConsole
提供项目 Web 站点和度量指示板。

Rational SoDA
在整个生命周期中自动化软件项目的文档编制工作

Rational Suite
提供最佳实践、工具和服务的完整而整合的生命周期解决方案。

Rational SUMMIT Ascendant
为交付企业级 IT 项目提供方法库。

Rational Team Unifying Platform
允许公共访问开发资产、需求和过程指导。

Rational Unified Process
经过验证的开发过程,可进行配置以满足您的项目要求。

需求分析

IBM Rational RequisitePro
需求和使用案例管理的强大、简便易用的集成产品,有助于促进更全面的通信,增强团队协作和降低项目风险。

IBM Rational Rose Data Modeler
数据库设计人员、分析人员、开发人员以及开发小组中的任何人能够协作的可视建模工具,从而能够捕获和共享企业需求,在整个流程中跟踪需求的变化。

IBM Rational Rose XDE Modeler
使设计人员能够使用统一建模语言(UML)来进行由模型驱动的开发。用户可以建立与平台无关的软件架构、企业需求、可重复使用的资产和管理级通信模型。

软件配置管理

Rational ClearCase
为大中型开发团队提供可靠的、可伸缩的和灵活的软件资产管理。

Rational ClearCase and MultiSite
为地域性分布式环境提供完整的软件资产管理。

Rational ClearCase Change Management Solution Enterprise Edition
为大中型项目和分布式团队提供集成的软件配置管理。

IBM Rational ClearCase LT
为中小型集中项目团队提供可靠的、入门级版本控制思路。

Rational ClearCase MultiSite
支持跨地域性分布式环境的并行开发方式。

Rational ClearQuest
在整个应用程序开发生命周期中提供灵活的缺陷和变更跟踪功能

Rational ClearQuest and MultiSite
为地域性分布式环境提供完整的缺陷和变更跟踪功能。

Rational ClearQuest MultiSite
支持整个分布式环境中的缺陷和变更跟踪。

软件质量

Rational Functional Tester
对 Java、Web 和基于 VS.NET WinForm 的应用程序进行高级自动化功能测试

Rational Functional Tester Extension for Terminal-based Applications
扩展了Rational Functional Tester,以支持基于终端的应用程序的测试。

Rational Manual Tester
使用新测试设计技术来改进人工测试设计和执行工作。

Rational Performance Tester
检查可变多用户负载下可接受的应用程序响应时间和可伸缩性。

Rational Purify for Linux and UNIX
为 Linux 和 UNIX提供了内存泄漏和内存损坏检测。

Rational Purify for Windows
为 Windows 提供了内存泄漏和内存损坏检测。

Rational PurifyPlus 企业版
为 Windows、Linux 和 UNIX 提供了运行时分析。

Rational PurifyPlus for Linux and UNIX
为 基于 Linux 和 Unix 的 Java 和 C/C 开发提供了分析工具集。

Rational PurifyPlus for Windows
为基于 Windows的Java、C/C 、Visual Basic 和 托管 .NET 开发提供了运行时分析。

Rational Robot
客户机/服务器应用程序的通用测试自动化工具。

Rational TestManager
提供开放、可扩展的测试管理。

Rational Test RealTime
支持嵌入式和实时的跨平台软件的组件测试和运行时分析。

传统语言和调试工具

Rational Ada Developer
为基于 Ada 的应用程序提供集成开发环境。

2007年09月26日

当许多不同的用户共享ClearQuest中的一个缺陷数据库时,管理员可能需要确保只有记录的所有者可以修改他或她自己的记录。本文提供了完成此方法的一个详细的过程

考虑一下以下情形:在一个缺陷数据库中有许多用户。这些用户可能来自许多不同项目的不同团队。为了避免混乱和系统中的不可预期的操作,IBM Rational ClearQuest管理员可能需要分隔每个用户的记录,因而只有记录的拥有者可以查看和修改他或她自己的记录。如果我们按照项目来分类这些用户,那么从一个个项目的观点来看,缺陷数据库对每个用户和每个项目看起来都是唯一的。

本文阐明了一个ClearQuest管理员可以如何通过提交者来分隔缺陷记录。本文提供了定制数据库过程的一步步的细节内容,并且包括一些如何对某些字段控制访问权限的钩子函数。

介绍
ClearQuest是
软件开发中变更管理的一个强有力的工具。它是高度可定制的,使用户能够灵活地控制他们的不同变更管理过程。ClearQuest也提供一些预定义的模板,它们从一些变更管理的最佳实践开始,将逐步地带领你领略此工具的微妙之处,

举例来说,当你应用其中一个标准模板来管理你的变更管理过程时,所有的团队成员缺省可以查询数据库中的所有记录。这可能会引起你的团队的混乱,或者可能会导致某些不可预期或不争取的操作--特别是当你在一个数据库中有多个项目时。你可能想通过所有者、项目、角色或组来分隔这些记录。ClearQuest提供了一个称为Security Context的机制,来帮助你达成此目标。

在我的例子中,我使用TestStudio作为模板,并解释了如何定制此模板,来按照提交者来分隔缺陷记录。通常的步骤如下:

  1. 定义你的隔离方针,然后创建适当的组。
  2. 创建一个security context记录类型。
  3. 修改你想要进行权限控制的记录类型,并增加一个reference字段。
  4. 如果需要,编写一些hook。
  5. 将模板应用于用户数据库,并做一些事先调整。

注意:此方法并不限于缺陷记录;它可以应用于任何你想进行权限控制的记录类型中。然而,下面的场景将集中在缺陷管理上。 

详细步骤
在此场景中,一个项目中的
测试人员只能查看和修改他们自己提交的记录,但是测试经理组的成员可以查看所有的记录。作为一个测试人员,每次你提交一个缺陷,缺陷的security context字段会自动地将你设置为所有者。只有ClearQuest管理员可以查看和修改security context记录类型。

此例子假定你熟悉ClearQuest定制过程--我将会浏览一下这些步骤,但不是详述幕后发生了什么。如果你对此过程比较陌生,请查看你的ClearQuest文档中的有关定制过程。

  1. 首先,你需要使用ClearQuest User Administration工具定义适当的组。我通常为每个测试人员定义一个组,然后创建一个测试经理组。在某种意义上,这些组代表了你的项目定义中的特定角色。例如,图1-5显示了关于我定义的组的详细内容:

 

图1:在ClearQuest中设置组

组xiaming、yangrong、zhanghan、yangrongwei和gengxueping都分别是单个测试人员的测试组。TestManager组是测试经理角色的组。

图2:TestManager组的定义

 

图3:ClearQuest管理员的SuperAdmin组的定义

 

图4:定义一个Guest组

如果经过ClearQuest管理员授权,你也可以设置一个Guest组,用于不在你的项目中的用户来访问那些缺陷记录。

图5:为一个单独测试人员设置一个组

这个单个测试人员组的例子包括成员组Guest和TestManager。

  1. 打开ClearQuest Designer,登录到你的目标schema数据库,并检出(check out)TestStudio模版进行编辑。增加一个新的状态无关记录类型,名为ACL(此过程的步骤如下面的图6-11所示)。此记录类型用作security context记录类型。

 

图6:增加一个新的状态无关记录类型

 

图7:ACL记录类型的字段定义

 

图8:ACL记录类型的action定义

 

图9:ACL记录类型的behavior定义

 

图10:ACL记录类型的主键定义

 

图11:ACL记录类型的form定义

 

  1. 修改缺省的缺陷记录类型,在缺陷记录类型中增加一个新字段。这个字段用于对security context记录的关联,如图12和13所示。

 

图12:缺陷记录中的ACL字段定义

 

图13:将ACL字段的behavior设置为USE_HOOK。

 

  1. 为了自动地分割缺陷,在关联字段上增加一个default value hook。尽管ClearQuest支持两种脚本语言(VBScript和Perl),但是以下脚本的例子使用了Perl:

 

sub acl_DefaultValue {
my($fieldname) = @_;

my $session;
my $username;

$session = $entity->GetSession();
$username = $session->GetUserLoginName();

$entity->SetFieldValue($fieldname, $username);
}

 

  1. 在关联字段上增加一个permission hook,在改变一个缺陷记录时设置控制权限。

 

sub acl_Permission {
my($fieldname, $username) = @_;
my $result;

my $userGroups, $sessionObj;
my $AuthorizedUserGroup = "SuperAdmin";

# By default, set this field readonly
$result = $CQPerlExt::CQ_READONLY;

$sessionObj = $entity->GetSession();
$userGroups = $sessionObj->GetUserGroups();

if(!@$userGroups) {
$result = $CQPerlExt::CQ_READONLY;
} else {
foreach $strAnGroup (@$userGroups) {
if ($strAnGroup eq $AuthorizedUserGroup){
$result = $CQPerlExt::CQ_OPTIONAL;
last;
}
}
}
return $result;
}

 

  1. 在缺陷记录的action上增加一个access control hook。在我的例子中,我在action Modify、Delete、Duplicate和Unduplicate上增加了此hook。为了更好地维护和重用代码,我使用record scripts来增强代码可重用性。

在一个action的access control hook中调用代码的一个例子如下:

sub Defect_AccessControl {
my($actioname, $actiontype, $username) = @_;
my $result;

my $params =
$actioname."\n".$actiontype."\n".$username;
$result = $entity-
>FireNamedHook("RS_AccessControl",$params);
return $result;
}

调用代码的一个例子,其是一个record script,名为RS_AccessControl

sub Defect_RS_AccessControl {
my($result);
my($param) = @_;
# record type name is Defect

if (ref ($param) eq "CQEventObject") {
# add your CQEventObject parameter handling code here
} elsif (ref (\$param) eq "SCALAR") {
$sessionObj = $entity->GetSession();

@params = split '\n',$param;
my $username = $params[2];
?$fieldInfo = $entity-
>GetFieldValue("Submitter");
$strSubmitterName = $fieldInfo-
>GetValue();

# test if the user belongs to group "TestManager"
$userGroups = $sessionObj->GetUserGroups();
$FlagYes = "yes";
$FlagNo = "no";
$AuthorizedUserGroup1 = "TestManager";
$AuthorizedUserGroup2 = "SuperAdmin";

$flag = $FlagNo;
if (!@$userGroups) {
$flag = $FlagNo;
} else {
foreach $strAnGroup (@$userGroups) {
if ( ($strAnGroup eq $AuthorizedUserGroup1) ||
($strAnGroup eq
$AuthorizedUserGroup2) )
{
$flag = $FlagYes;
last;
}
}
}

# test if the user is same as the
submitter or belongs to group "TestManager"
if (( $username eq $strSubmitterName)||($flag eq $FlagYes)){
$result = 1;
} else {
$result = 0;
}
} else {
# add your handling code for other type parameters here, for example:
# die("Unknown parameter type");
}
return $result;
}

 

  1. 使用用户组修改ACL记录类型上的所有action的控制权限,如图14所示。

 

图14:设置控制权限

这显示了只有在"SuperAdmin"组中的成员可以对ACL记录类型操作。

  1. 检入schema,并将此涉及应用到你的用户数据库中。
  2. 最后,ClearQuest管理员需要手工地增加一些security context记录,这些记录与每个测试人员组关联在一起(图15)。

 

图15:security context记录

注释:在此设计中,ACL记录的名字应当与提交者的userid完全一样。例如,在图15中,如果一个提交者的userid是xiaming,那么相应的ACL记录的名字也是xiaming。

不错,这并不是那么难,是不是?现在,你可以自己尝试设计,来看一下缺陷记录是否完全按照提交者分隔开了。如果系统没有准确地分隔缺陷,重新再进行这些步骤。祝你好运!

 

2007年09月25日

如何在脚本中做关联 (Correlation)
当录制脚本时,VuGen会拦截client端(浏览器)与server端(网站服务器)之间的对话,并且通通记录下来,产生脚本。在VuGen的Recording Log中,您可以找到浏览器与服务器之间所有的对话,包含通讯内容、日期、时间、浏览器的请求、服务器的响应内容等等。脚本和Recording Log最大的差别在于,脚本只记录了client端要对server端所说的话,而Recording Log则是完整纪录二者的对话。

当执行脚本时,您可以把VuGen想象成是一个演员,它伪装成浏览器,然后根据脚本,把当初真的浏览器所说过的话,再对网站伺服器重新说一遍,VuGen企图骗过服务器,让服务器以为它就是当初的浏览器,然后把网站内容传送给VuGen。
所以纪录在脚本中要跟服务器所说的话,完全与当初录制时所说的一样,是写死的(hard-coded)。这样的作法在遇到有些比较聪明的服务器时,还是会失效。这时就需要透过「关联(correlation)」的做法来让VuGen可以再次成功地骗过服务器。
何谓关联(correlation)?
所谓的关联(correlation)就是把脚本中某些写死的(hard-coded)数据,转变成是撷取自服务器所送的、动态的、每次都不一样的数据。
举一个常见的例子,刚刚提到有些比较聪明的服务器,这些服务器在每个浏览器第一次跟它要数据时,都会在数据中夹带一个唯一的辨识码,接下来就会利用这个辨识码来辨识跟它要数据的是不是同一个浏览器。一般称这个辨识码为Session ID。对于每个新的交易,服务器都会产生新的Session ID给浏览器。这也就是为什么执行脚本会失败的原因,因为VuGen还是用旧的Session ID向服务器要数据,服务器会发现这个Session ID是失效的或是它根本不认识这个Session ID,当然就不会传送正确的网页数据给VuGen了。
下面的图示说明了这样的情形:
当录制脚本时,浏览器送出网页A的请求,服务器将网页A的内容传送给浏览器,并且夹带了一个ID=123的数据,当浏览器再送出网页B的情求时,这时就要用到ID=123的数据,服务器才会认为这是合法的请求,并且把网页B的内容送回给浏览器。
在执行脚本时会发生什么状况?浏览器再送出网页B的请求时,用的还是当初录制的ID=123的数据,而不是用服务器新给的ID=456,整个脚本的执行就会失败。

要对付这种服务器,我们必须想办法找出这个Session ID到底是什么、位于何处,然后把它撷取下来,放到某个参数中,并且取代掉脚本中有用到Session ID的部份,这样就可以成功骗过服务器,正确地完成整个交易了。
哪些错误代表着我应该做关联(correlation)?
假如脚本需要关联(correlation),在还没做之前是不会执行通过的,也就是说会有错误讯息发生。不过,很不幸地,并没有任何特定的错误讯息是和关联(correlation)有关系的。会出现什么错误讯息,与系统实做的错误处理机制有关。错误讯息有可能会提醒您要重新登入,但是也有可能直接就显示HTTP 404的错误讯息。
要如何做关联(correlation)?
关联(correlation)函数
关联(correlation)会用到下列的函数:

2007年09月24日

我们应该如何面队国外抛送过来的包呢?难道就就是长期以“包工制”形式一直做下去?

印度一家公司软件工程师为软件企业产品开发人员讲授如何管理软件测试外包项目。

我们应该如何面队国外抛送过来的包呢?难道就是长期以“包工制”形式一直做下去?

答案是否定的。很显然,我们的“包工制”外包项目就是靠实现服务赚钱,如果长此以往,那么我们做的只是低层次的IT企业或软件企业,这种发展趋势,决不是中国企业、中国政府所希望发展趋势。

那么我们应该如何逐步演变“包工制”,如何借“外包”把中国的软件企业带到一个更高的境界?

项目外包的核心理念就是“做你最拿手的,其余的让别人去做”。因此,我们要做好外包项目,也需要从这个理念开始。我们不是没包接,而是没有实力和规模接大包。所以我们要能做好外包项目,做大外包项目,首先我们要有自己最拿手最擅长的招。印度的规模编码设计、爱尔兰的本地化都是在IT市场竞争中获胜了的接包的招。可是我们国内企业,还需要磨练,还需要更强更深的技术能力和项目管理能力等招术。

软件外包测试的兴起对国内软件本地化企业意味着什么?笔者认为,意味着更多的机会,争取更多软件外包国际市场份额的机会。

笔者试图通过多年来从事中高端软件外包工作管理的经历,以一个外包测试项目为例,总结了一些外包测试项目的经验,与读者共飨,以期达到抛砖引玉,共同提高外包行业管理能力的目的。限于篇幅,本文仅对测试外包中的风险管理和沟通管理做一个简单的整理。

 

软件测试外包特性

与国内一直以来比较轻视软件测试工作不同,在很多欧美软件企业中,测试(质量控制)是一件非常重要的工程工作。国内企业一般在从事软件项目开发的时候,更多的是由开发人员或者客户人员在开发完成之后才进行一些简单的功能测试工作,很少采用专业的测试团队,开发与测试的比例在4:1以上,甚至高于10:1。因此,多数中国软件的质量水准相对要低。

与此相反的,在欧美企业中,质量管理人员(包括事后的质量控制和事前的质量保证)的地位却高的多。测试也作为一个非常独立的职业。在IBM、Microsoft等开发大型系统软件公司,很多重要项目的开发测试人员的比例能够达到1:2,甚至1:4。测试活动贯穿于整个开发生命周期,甚至会比普通开发人员更早介入项目。测试也是一门更讲究科学方法的工程活动,测试的种类也包括单元测试、集成测试、功能测试、性能测试、β测试、验收测试等等。

本文所介绍项目的客户是美国一家知名的金融业软件及服务供应商。由于金融业的特点,对于软件的可靠性、稳定性等质量要求尤其的高。该公司从2005年开始将部门开发和测试工作外包到中国地区,由笔者所在公司在北京和上海分别成立了两个团队,采用一种类似ODC(Offshore Development Center,离岸外包中心)的模式。笔者在过去一年半的时间内负责北京团队的建设和管理,从最初的一个测试人员和两个开发人员发展到现在的十几人的团队(笔者由于公司内部工作安排已经于去年下半年离开该项目)。从最初的从事简单的软件产品的安装和数据移植测试,到现在从事其核心产品的全面功能测试和自动化测试,团队的工作越来越受到客户的认可,在客户的软件开发过程中的重要性也越来越高。按照人员数量来计算,该项目的测试与开发人员的比例略高于1:1。

 

软件测试外包的风险管理

软件外包工作由于天然存在的地理、语言文化差别,其失败的风险几率就大的多了。所以从事外包的管理人员在项目启动之前尤其要对项目中可能存在的风险因素有一个比较全面地识别和分析。比较好的一种风险识别方法是结构化的头脑风暴法,通过集思广益找出所有可能影响到项目进度、成本、质量的因素。一个非常重要的风险因素的来源是项目计划的假设和约束,一旦项目成功所作的假设不能达到,这些就会成为未来影响项目正常进展的问题

一旦识别出尽可能多的风险因素之后,需要对这些因素进行评估,并不是所有的风险都需要去规避,所以必须分析哪些风险会对项目产生重大影响,哪些风险发生的可能性非常高。根据这些分析结果排出项目中优先级比较高的风险因素(比如Top 10列表),然后针对这些风险分别找出规避的措施以及风险发生时的应对举措。

针对本文中提到的项目,从风险识别的角度来说,自然灾害和战争等也是被识别的一些因素,由于属于不可抗力,所以一般并不列入项目风险管理计划,但不要因此在风险识别阶段就将其排除。而时差、测试人员的英语表达能力、中美的文化差异、网络的稳定性和速度(想想前段时间的海底光缆事故)、数据传输的安全性、节假日安排这些做国内项目的时候几乎不会考虑的因素可能会是对项目的致命威胁。

对于每个风险都能找到一定的规避和减少损失的措施,笔者在项目启动初期花费了较多时间准备金融领域的业务知识,通过夜间在家加班与客户建立了良好的信任关系,建立了定期的会议和汇报机制,很短时间之后就确保了团队正点上下班也不会存在时差所带来的障碍。

软件测试外包的沟通管理

学习过PMP/PMBOK的朋友可能都知道这么一个事实,一个项目经理85%的时间都用在各方面的沟通交流上。很多项目出现问题都不是在技术上碰到难题,而更多的是由于沟通不畅引发的后果。

以前做国内项目的时候一说沟通,很多人就想到要和客户一起去吃饭喝酒。相对这种非正式的个人之间的沟通,在外包项目管理中更需要建立流畅的正式沟通渠道。这种正式的沟通渠道包括但不限于定期的电视电话会议,正式邮件往来,通过IM的工作聊天,统一的错误报告机制(Bug跟踪系统及其他)等。一个非常重要但经常被忽视的沟通渠道就是文档,尤其是项目计划,不少项目经理都只是把项目计划当作前期不得不完成的一个文档,到项目执行过程中即使再去阅读也只是关注进度表部分。其实项目计划包含了众多内容,除了进度表外,还包括上面提到的风险管理计划,以及项目的范围界定和各种项目管理流程(包括需求变更管理流程等),它在一定意义上是合同的一部分,所以及时的更新、审核(Review)和批准(Approval)不仅仅对项目的监控非常重要,对于开发方来说也是很好的法律上的保护。

正式的沟通还有一点非常需要注意的就是要保证良好的反馈机制。我们都知道信息在传递的过程中会受到噪音的干扰,尤其是在多次传递的情况下更容易失真。所以在建立沟通渠道的时候要建立起方便有效的反馈途径。对于重要的文档,一定要有合适的审核和批准机制。更为有效的一种机制还是利用类似于Mercury的Quality Center这样的工具,由于其内置了工作流,所以任何的问题都必须按照一定的流程进行处理,避免了因为工作忙而忘记了一些重要工作的可能。

最后需要提醒的一点的是,要保留一切沟通过程的证据。美国的金融监管部门对金融业内部的所有电子邮件要求保留至少5年以上以备检查。在项目中证据的保留一方面是对自己权益的保护,另一方面也是保持完整的沟通记录有利于后期出现问题的顺利解决。

链 接

风险管理(RSKM)

作为PMBOK九大知识领域中的一个,风险管理(RSKM)在现代项目管理中越来越受到管理人员的重视。在CMM迁移到CMMI的时候,风险管理被特意作为一个过程域从项目监控(PMC)中独立出来,风险管理的重要性也由此可见一斑。有人说项目管理其实就是一种风险管理,虽然有点言过其实,不过对于风险因素的预见、计划和适当的应付对项目管理确实起到至关重要的作用。

2007年09月21日

一切都是从一次不成功的电话面试开始的。正在学车的我刚刚通过了桩考,心情不错,正在大厅里悠哉游哉地等着刷卡。忽然一个陌生的电话打进来,对面介绍说是一家S开头的手机操作系统公司,刚在北京设立了研发中心。

  七月初的时候在CSDN看到消息,说这家公司有很宏伟的中国计划,浏览职位时发现Senior Trainer的职位,于是投了份简历。因为开发方向不同,所以并没有抱太大希望。没想到两个多月之后竟然要我进行电话面试。其实,当时很礼貌的拒绝了,也不会有之后的不愉快,但是谁叫我当时刚过桩考呢,我在心情好的时候不太善于拒绝。

  一开始倒很正常,对方的语气很职业,语速很快,听不出什么感情来,让我觉得像面对一部机器。下面是一些对话的片段,凭记忆写下来的,不一定准确,但我尽量反映对话的原貌:

  Q:你是不是熟悉C++?

  A:呃,还行,用了五六年了……(开发语言这东西,越到后来就越不敢说熟悉,模板、泛型,你能说自己熟悉吗?倒是很多刚会写printf的人说自己精通C++)

  Q:那好,那么下面我问几个关于C++的问题,请问什么是Hash Table?

  A:Hash Table?哦,哈希表啊,一种数据结构,经常用于检索……(此处略去三百字)

  Q:哦,那好……(耳机里传来敲键盘的声音)

  A:请问您是HR还是技术人员?(听语气像是HR,但是怎么问起技术问题来了?我解释那么多,能听明白吗?忽然有一种被人耍的感觉,因为我解释什么是哈希表的时候的确是按照开发时的心得来回答的……可是人家根本听不懂)

  Q:我是HR,可能有些听不懂你的答案。那么第二个问题:您觉得下面关于原子操作的描述正确的是?

  A:……(听不懂还问,忽然就变成幸运五十二了)

  Q:A,一同做一些事情,或者什么也不做;B,……;C,……(B和C两个答案我根本没听进去,因为我觉得这几个答案都不靠谱)

  A:我觉得原子操作是这样,在多线程的环境中,有些多条指令操作不能够被打断,所以我们需要设置一个原子操作,让其他线程不打断这个操作。

  Q:……哦,那么你觉得哪个答案是对的?

  A:我觉得哪个答案都不对……(鸡同鸭讲)

  Q:那你也要选一个……(听口气对方也很郁闷,本来嘛,除了ABC,她也不知道我在说什么)

  A:那么选A吧……(我已经不太有耐心了)

  Q:哦,那选A……(又听到敲击键盘的声音)

  Q:那么下一个问题,请问下面对回调函数描述正确的是:

  A:(轻笑了一下)

  Q:您觉得这个问题很有趣吗?(语气很警惕,还有点愠怒)

  A:我觉得这个问题很无趣。

  Q:这是什么意思?(完全是质问的口气)

  A:哦,没什么,您继续……(基本的涵养还是应该有的,可是如此不客气的HR我还是第一次遇到,佩服)

  Q:(下面仍是开心辞典式的三选一问题,包括同步过程、回调函数、纯虚函数等技术点)

  A:……(略去具体的问题和回答,因为我已经不太有耐心做一个技术的探讨了,随便选一个得了。反正即使选错了,我还是会用纯虚函数写程序,C++里没有的接口我都会用……如果大家好好上了大学的C++课程,应该不会弄错。可是对于一个有六年工作经验的人来说,我已经没兴趣温习这些大学课程了)

  到这里大家也许可以看出来了,我已经对这次面试失去了信心,原因有三点:

  一, 非技术人员来问技术人员关于技术的问题,技术人员无法让对方了解自己的技术背景,因为除了固定的选项外,考官根本听不懂面试者的解释,其实这部分才是最有价值的(我也面试过别人,我就喜欢开个头,然后听人家说,这样可以更全面的了解这个人的技术背景,毕竟每个人都会捡自己最擅长的说)。

  二, 用考应届毕业生的题目来考我这个有六年工作经验的人,我的确有一种被轻视的感觉,很多更大的软件公司也会考应试者的基本功,可一般都是放在一个具体的开发环境中,我的确没遇到过这种考察方式,如果是我的话,我会把这些放在正式面试中,写两个小程序,什么都知道了。

  三, 当应试者对题目提出质疑时,HR应该有自己最基本的职业态度,而不应该立刻火冒三丈,而是应该老实承认面试流程中的问题,毕竟有些事情不是HR就能够决定的。

  事情到了这个,大家已经能够看到结果了,但是还没有结束。HR对自己的情绪还是有很强的控制能力的,很快开始问我是不是可以接受英文面试。

  电话面试我通过了?可是我还没决定是不是继续参与你们的面试,因为电话面试并没有给我留下好的印象。于是该我反击了:

  A:那么您能不能先告诉我,Senior Trainer这个职位大概的工资范围?

  Q:对不起,我在这个环节只能告诉您,您是不是能够进入下一轮面试。

  A:(我还不一定要参加呢)那您能不能告诉我一个大概的范围,我好决定是不是继续参加面试。(话已经很清楚了)

  Q:对不起,这属于公司的机密。(对于我的意向丝毫没有任何担心)

  A:难道我只有拿到Offer时才能知道我挣多少钱吗?(我参加面试也是有成本的)

  Q:对不起,我帮不了您。(语气很漠然,伊朗和朝鲜怎么没请这位去做核问题的谈判代表?)

  A:那好吧,谢谢您,我对贵公司不感兴趣……(其实这句话是被逼的,我的确无法忍受一个傲慢的HR这么继续折磨我两三次)

  我并不是故意针对这家公司的HR,但是这是一个非常典型的例子,因为HR的所作所为让应聘者对这个公司失去兴趣。因为这家公司还是很有价值的,被世界著名手机厂商N公司控股,智能手机操作系统占有率第一,刚刚开始设立中国公司,未来几年会高速发展。可能也是因为这些因素,让HR觉得他们拥有足够的资源,可以网罗天下英雄。所以,对我这样的小鱼小虾并不是十分在乎。

  可是很多在中国设立研发中心的欧美公司,几乎都在抱怨,无法招到合适的人,毕竟在哪个国家,有足够资历的专业技术人员永远都不会过剩。毕竟S公司刚进入中国,可能一年之后,这家公司的HR会后悔一年前打过的一些电话,她放弃得太早了,也放弃得太傲慢了,一致让某些人从此对这个公司失去了兴趣。

  抛开这个具体的事例,作为一个单纯的程序员,我想对单纯的HR讲几件事,希望能够让高高在上的HR换一个观察事物的角度,至少会让以后的面试者和应试者在面试之后都有个好心情吧。

  一、 HR是公司的名片

  估计这句话已经被无数的管理者说过了,但是有几个HR真的理解这句话呢?面试者进入一家公司,遇到的第一个人肯定是HR。很大程度上,面试者会以第一个人判断整家公司的氛围和人际关系。

  有的HR不太注意细节,比如把面试时间约在很热的下午,或者靠近午餐的时间,而当完成面试后,并不会安排午餐,甚至连安慰的话也不说。我一个朋友说过这样一个故事:一个面试者在中午12点半完成部门经理的面试后,回去找HR。HR正捧着盒饭吃得很香,看到面试者后,说:“今天的面试就到这里,你先回去吧。”

  面试者:“@#¥%……&×”

  毕竟,面试者会以未来的同事看待所遇到的HR,如果HR对面试者的感受漠不关心,那么在将来入职后,也会有很大的麻烦,比如HR会拖延办理各种手续等。

  二、 面试不是在施舍

  曾经在一个人力资源论坛上听过这样一句话:“中国的人才已经不再便宜了!”深以为然,谈话者也许说得是人民币升值的影响。但这句话对刚进入中国的外资企业有很强的警示意义,管理者容易看到中国工程师的低工资,但是却容易忽视中国同样稀缺高端的人才。很多时候,都是几家公司在争抢一个人才,所以除了工资外,公司也许会付出更多额外的条件,比如支付北大MBA的学费等等……

  中国的媒体总是在渲染就业难,其实,这是一个强调片面的论题。应届毕业生找工作的确比较难一些,但是很少有人到第二年仍找不到工作。以计算机专业的学生为例,工作两年后,对开发语言和某一个专业领域有深入了解后,他们就可以很容易地寻找工作了。这时,待遇、进一步的发展机会、工作环境就会成为大家关注的重点。

  所以,在面试时,HR一定不要有“为面试者提供一份薪水”的想法,因为也许还有更好的机会在等着你面试的人。HR的职责是:为公司寻找最合适的人,而不是拿公司的资源去施舍一个失去工作的人。

  外包公司在这方面的感觉也许会更强烈一些。经历了几年对资源的过度开发之后,工程师们对外包公司的评价并不是十分正面。两年之前,会有陌生的电话突然打进来,让你去上地的某处去面试,可是我压根没投过简历啊?

  你不为别人考虑的最后结果就是,别人也不会为你考虑。所以,从那里之后,我就把自己网上的简历关掉了。整个世界清净了。

  三、 选择是双向的

  这也是老调重弹,不过看起来似乎还是有重弹的必要。还是拿S公司说事吧,我从来没有听说过哪个公司会不告知面试者未来的薪水。这其实是一种不尊重,好像面试者是菜市场中的鱼虾,由买菜的人任意挑选,间或对肥瘦进行一些评价。而事实是,这些鱼虾其实是在海里,他们有选择去水族馆、某家的鱼缸,或者继续游在海里的权利。当然,也会有人以很好的饲料将某些鱼诱骗到厨房中。可是,一群平均智商在120左右的工程师鱼会上几次当呢?

  面对鱼钩,工程师鱼问一下未来的伙食情况,似乎不是件大逆不道、触及公司机密的大事件吧?在这几年中,中国工程师的待遇已经得到很大改善,并不是每家外国公司都能开出一个吓死人的薪水了。我也遇到过一些信誓旦旦要提供“丰厚”薪水、良好发展环境的公司,经过多次接触,小心翼翼地问一下未来可能的薪水,某高管以很豪爽的声音报出了一个数字。小鱼我摇了摇耳朵,因为听到的数字比现在的薪水还要低一些……

  当然,薪水不是唯一的条件,很多要争取“最佳雇主”的公司也在很多方面下了工夫,总结起来有下面几条:设施完善、充满零食饮料的厨房;由公司组织的礼仪、外语培训;拓展训练。本着少花钱多办事的原则,这些活动在很多公司都办得走了样。

  其实与其在这些面子上做工程,还不如多一个微笑,少一点指责、让雇员少加点班。办公室的温度不取决于中央空调,而取决于人与人之间的距离。

  另外还有一个口碑的问题,曾经有一家特立独行的通信业公司,他们的HR在晚上八点半的时候给我打电话,很客气地问我是不是对他们公司的北研所感兴趣。我很欣赏那个HR的态度,他是用一种朋友的语气在和我交流,这点让我很感激。我客气地回绝了,真正的理由我并没有告诉他,在我吃完晚饭的时候,他仍然在办公室里,单凭这一点,我也不会考虑这家公司。加之对这家公司的独特加班机制、员工过劳死问题早有耳闻,一个随性且懒散的人恐怕不适合这样的公司。恐怕大部分人也不适合这样的公司……

  四、 人才不是海里的鱼虾

  天地不仁,以万物为刍狗。圣人不仁,以百姓为刍狗。HR不仁,以面试者为刍狗……后边这句是我加的,幸好李耳先生不会告我侵犯版权。大多数HR的确不会把工程师当作刍狗,但是当面对永远都看不完的简历时,HR的确会有一种错觉,觉得人才是海里的鱼虾,怎么捞也捞不完。真的是这样吗?

  人才有一个成长的周期,工作年限越长,经验积累越丰富,而如果年龄恰好在30岁左右的话,这样的人还有足够的精力来进行工作。如果按这个标准来看,真正能够符合要求的人却并不多。人世间的石猴子可能很多,但在老君的炼丹炉里炼出火眼金睛的却只有一个,那么,人才的成长是有偶然性的,或者说属于不可再生资源。

  外包公司在过去的几年中,颇有点泥沙俱下的味道,无论鱼虾是否成熟,一律捞上来再说。这种破坏性的捕捞是无法持续的,原因很显然,公司只注意捕捞,却从来不关心人才的培育。当人才被捕捞上来后,公司也不会培养他,只是拿过来就用,一个工程师可能几年都在做相同的事情,这样的情况下,工程师的流动率大就是很正常的了。一旦工程师离开一家公司,那么回到这家公司的概率就会降到很小,那么公司就永远失去了这个人才。

  很多公司在招人时很大方,可是对已经招进来的人却关心不够。在招聘需要的人才时,公司可以提供很高的薪水;可是对于工作出色的工程师,加薪的幅度却小得可怜。美其名曰,保持公司内部的工资平均。这样的情况,大家都会将跳槽作为加薪的第一选择,人才流动率不高才怪。

  鱼虾总有捞完的那一天,更何况越高端的人才,培养时间就越长,一旦失去了,公司弥补空缺的代价就会极大。

  说了这么多,回到人力资源的开头——招聘的环节,HR如果轻易放弃了一个人,那么这个人对于这家公司来说,恐怕就永远失去了。

  五、 谁来支付面试者的成本?

  很多管理者都在高喊:“招聘是有成本的!”那么,应聘是不是有成本呢?有的成本是看得到的,比如交通费用、误工费等。而有些成本是看不到的,比如收集相关公司的资料,进行针对性的学习等。我经历了很多次面试,只有在刚毕业时,一家韩国公司支付过面试的交通费用,尽管那家公司已经不存在了,但我仍然还记得那次面试。

  很多国外企业都会支付面试者的交通费用,很遗憾的是,这种习惯却没有在中国企业中发扬光大。表面上的成本还比较好计算,可是为了面试过程付出的其他成本恐怕很难计算,而且也无法得到补偿。在这种情况下,面试者要求一些对等的信息,比如职位、薪水等,从而衡量是否参加面试。怎么说也不算十分过分的事情,很多HR会很热情地介绍公司的情况,这也是对公司的一个宣传,还会询问应聘者是否还有问题。像S公司这样,拒绝回答任何问题的HR还真是不多见。这样的HR只考虑自己的招聘成本,却不考虑应聘成本。也许HR会说,我的时间也是金钱。如果按照工资折算的话,应聘者的时间恐怕会更宝贵一些。

  六、 HR是资源使用者,而不是垄断者

  在公司里,很多人都会感觉HR很牛,可是HR为什么很牛?因为他们掌握资源,比如他们了解所有人的薪水,每个人办理社会保险、公积金时都会有求于HR,公司管理者往往会关注HR,从而让HR称为距离老板最近的人,自然会有些狐假虎威。综上所述,HR掌握了公司的大部分行政资源。可是很少有人注意到,其实HR只是这些资源的使用者,却并不是拥有者。

  首先,HR不创造价值,驱动一个公司不断向前的是那些呆头呆脑的工程师,从本质上说,HR并不是这些工程师的管理者,而是服务者。HR的工作性质也是如此,发放工资,管理保险和公积金。HR有权管理这些事务,但是没有权利利用这些去限制工程师。当然也有部分公司,利用这些来限制人员的正常流动,不过随着规则的完善,这种情况越来越少了。

  其次,HR掌握必须的社会资源,有的HR会抱怨,工程师连个简单的保险计算公式都不清楚;可是从来不会有工程师抱怨HR不会写快速排序。原因是工程师需要上保险,而HR不需要快速排序。但是如果每个软件公司招聘工程师时都要求“熟悉社会保险计算”,那么HR是不是会裁剪职位呢?

  七、 真正的职业不是体现在语气上

  在一个专家时代,说别人不专业,恐怕是最大的一种侮辱。我无意说谁不专业,只是有些HR的专业只是停留在表面上。比如语速非常快,语气冷冰冰的,这样固然能够产生一种威严,可是这种漠然会让人心寒。毕竟HR是与人打交道的,人的心理活动非常复杂,无法通过量化计算来完整描述。这也是为什么人可以编软件,软件无法编人的原因。

  世界就是这么奇怪,整天和冷冰冰的机器打交道的工程师会对人真诚,知无不言,言无不尽。而整天与人打交道、整天嘴上挂着沟通的HR却总是一副拒人千里的面孔。于是很多人宁可和机器打交道,因为机器不会给你脸子看,偶尔死机时,你还可以直接捅它的屁股,而这一套对人似乎行不通……

  HR的工作也需要专业的精神,现在很多HR都是来自行政、文员,甚至前台,当然,做不好HR的人恐怕也做不好前台。但是认为HR不需要专业精神的观点,是十分不对的。还是拿S公司做例子,难道S公司没有技术人员吗?如果一定要把技术面试放在电话面试的阶段,那么能不能让能够听懂技术的人坐在一边?毕竟真正的技术不是几个似是而非的选项能够涵盖的,而且我也确实觉得出题的技术人员水平恐怕也不高,要么是技术水平不高,要么是文字表达水平不高,还有一种可能性,就是直接从网上找的面试题……

  无论是哪种可能性,我都无法相信这次面试的客观性。让一个听不懂技术的人来判断一个技术人员的水平,这个近似儿戏的事情,只会发生在充满儿戏的公司里。一个真正的技术人员恐怕对这种儿戏公司都无法保持一种尊敬。我宁可去尊敬欢乐谷,尽管他们也很儿戏,但至少他们的安全设施还是很专业的,一点都不儿戏。

  关于专业的问题,我也有一个建议,不要相信ABC的选项,找一个真正了解技术的人来做面试,因为面试者也会通过和考官的交流来了解公司的技术水平。还是那句话,选择是双向的。

  好了,说这么多吧,我很少有攻击性的言论,这篇文字可能会引起一些争论。但是,我的本意是想让大家在面试之后更快乐一些,至于调味加的是不是够辛辣,就是仁者见仁,智者见智了,四川人说不辣,上海人说太咸,山东人说太甜,这个就只好留给各位看官品评了。最后说一句,文章中提到的一些公司,请大家不要对号入座。

  让大家冷暖自知吧。

2007年09月20日

或许每个软件从业者都有从学习控制台应用程序到学习可视化编程的转变过程,控制台应用程序的优点在于可以方便的练习某个语言的语法和开发习惯(如.net和java),而可视化编程的学习又可以非常方便开发出各类人机对话界面(HMI)。可视化编程或许是一个初学者开始对软件感兴趣的开始,也可能是一个软件学习的里程碑点,因为我们可以使用各类软件集成开发环境(IDE)方便的在现成的界面窗口上拖放各类组件(Component),这类组件包括我们常见的按钮(Button),单选按钮(Radio Button),复选框等(Checkbox)。这样的拖放式开发方式不但方便,而且窗口会立竿见影的显示在我们的面前,这对于一个软件初学者而言或许是一件非常有成就感的事情。

  但是很多软件学习者在学习可视化开发的过程中,只是非常表面的来理解可视化编程,他们可能认为能够使用拖放方式完成一个界面就非常值得称道,但是很少有人会认真的去理解编程语言对于可视化编程组件的支持和整合,在Softworks软件人才培训中心的两年教学过程,我深刻的感受到了这一点,因此下文将会结合我的教学经验来讲解可视化编程过程中最为关键的“事件驱动模型”。

  1.什么是事件驱动模型?

  在讲解事件驱动模型之前,我们现在看看事件驱动模型的三大要素:

  ·事件源:能够接收外部事件的源体。

  ·侦听器:能够接收事件源通知的对象。

  ·事件处理程序:用于处理事件的对象。

  学员应该要理解任何基于事件驱动模型的开发技术都包含以上三大要素,不管是.net还是java技术,甚至是以前我们使用的Visual Basic和Delphi语言都有基于以上三大要素的事件驱动模型开发流程。

  现在我们来看一个生活中的示例,如果有一天你走在路上一不小心被天上掉下来的花瓶砸到了,并且晕死了过去。那么整个过程其实就是一个事件处理流程,而且我们可以非常方便的分析出刚才所提到的事件驱动模型中的三大要素。

  1.被砸晕的这个人其实就是事件源,因为他是能够接受到外部的事件的源体。

  2.侦听器就是这个人的大脑神经,因为它会感知到疼痛。

  3.事件处理就是这个人晕死了过去。

  由于事件驱动模型在我们日常生活中是无处不在的,因此Java和其他的编程语言都将这一过程运用到了可视化编程中了。

  2.Java编程语言中的事件驱动模型

  在Java编程技术中,最常用的可视化编程当属Java Swing技术,Java Swing为开发者提供了很多现成的组件,如:按钮(JButton),单选按钮(JRadioButton)等。为了管理用户与组成程序图形用户界面的组件间的交互,必须理解在Java中如何处理事件。

  假设用户单击了程序图形用户界面中的一个按钮,其实该按钮就是这个事件的源(可以引发事件的物体)。所有的Java Swing对象都有感知自己被操作的能力,因此JButton按钮也具有这样能力。一个事件通常必须有一个源对象,这里就是JButton对象。当单击按钮时,JButton组件类会生成一个用于存放该事件参数的ActionEvent的对象,该对象包含了事件及事件源的信息。图1-1显示了这种机制。

  

  图 1-1

  当JButton感知到自己被点击以后会将这种感觉传递给某个侦听器对象,该侦听器对象原先已被告知对该类事件感兴趣,侦听器对象仅是一种侦听特定事件的对象。这里的“将事件传递给侦听器”仅意味着事件源调用侦听器对象中的一个特定方法,并以事件对象作为实参。侦听器对象可以侦听一个特定对象的事件(比如一个按钮)。

  其实可以使任何类的对象成为侦听器对象,只要使该类实现侦听器接口。你将会发现有各种各样的侦听器接口,以满足不同类型事件的需要。在这个单击按钮的例子中,需要实现ActionListener接口以便接收按钮事件。在侦听器接口声明的方法中,实现了接受这个事件对象并响应该事件的代码。在本例中,在事件发生时,调用了ActionListener接口中的actionPerformed()方法。每种侦听器接口都定义了特定的方法,用来接收该侦听器计划要处理的事件。

  仅仅实现侦听器接口还不足以将侦听器对象连接到事件源上,仍需要把侦听器与希望处理的事件单个源或多个源连接起来。通过调用事件源对象的特定方法,可以注册带有事件源的侦听器对象。例如,为了注册侦听单击按钮事件的侦听器,需要调用JButton对象的addActionListener()方法,该操作可以使侦听对象和事件源绑定。

  每个事件响应时只涉及到对该事件感兴趣的侦听器。由于侦听器只要求实现一个合适的接口,所以实际上,可以在任何希望的地方接收和处理事件。在Java中使用侦听器对象处理事件的方式,称为委托事件模型,这是因为对于诸如按钮这种组件引起的事件响应,并不是由引起事件的对象本身处理,而是委托独立的侦听器对象进行处理,刚才的actionPerformed()其实就是一个委托处理方法。现在让我们来看一下,JButton是如何将用户的点击转化成方法处理的(如图1-2)。

  图1-2

  

  JButton组件初始化代码片断:

 

   privatevoidinitialize() {

       frame=newJFrame();

       frame.getContentPane ().setLayout (null);

       frame.setBounds (100, 100, 247, 165);

       frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

       frame.setTitle ("事件驱动程序");

       //btnPress就是这次点击操作中的事件源

       btnPress=newJButton();

       btnPress.setText ("Press");

       btnPress.setName ("Press");

       btnPress.setBounds (63, 98, 99, 23);

       //向事件源btnPress植入侦听器对象ButtonEventHandler

       btnPress.addActionListener (newButtonEventHandler(this));

       frame.getContentPane ().add(btnPress);

       frame.getContentPane ().add(txtMessage);

   }

  侦听器创建的代码片断:

 

//侦听器对象ButtonEventHandler(用来侦听按钮的点击操作)

   classButtonEventHandlerimplementsActionListener {

       //窗体对象

       privateEventDemoform=null;

       //通过构造体传入窗体对象,

        //作用在于让侦听器对象明白事件源处于

       //哪个窗体容器中

       publicButtonEventHandler(EventDemo form) {

           this.form= form;

       }

 

       //委托方法

       publicvoidactionPerformed(ActionEvent e) {

         //该方法将会把事件的处理权交给窗体容器类的

//btnPress_Click方法处理。

           this.form.btnPress_Click(e);

       }

   }

 

 真正的事件处理代码片断:

    /**

    *按钮btnPress的事件处理方法。

    *

    *@parame事件参数

    */

   privatevoidbtnPress_Click(ActionEvent e) {

       

       String message ="你点击的按钮名叫:"

           + ((JButton) e.getSource()).getName();

       

       this.txtMessage.setText(message);

   }

 

  代码工作原理:

  JButton组件初始化代码片断已经明确阐述了按钮被创建后放置于窗体上,关键在于本代码片断的以下语句:

  btnPress.addActionListener(new ButtonEventHandler(this));

  该语句就是向事件源植入了侦听器对象ButtonEventHandler,该类实现了ActionListener结构,因此JButton类的对象btnPress这个时候已经具有了处理用户点击按钮的能力了。

  当用户点击btnPress这个按钮的时候,按钮对象会直接把这次点击感觉传递给ButtonEventHandler的actionPerformed方法处理,为养成较好的编程习惯,我们中心并不建议学员直接在该委托方法中编写代码,而是需要将该事件处理再次转发给窗体中的某个方法来处理,这个方法的命名也必须是有规则的,就是事件源名+下划线+事件名(btnPress_Click),并且该方法必须具有事件参数ActionEvent,因为在该对象中明确指明了,哪个按钮受到了点击了。e.getSource()方法返回了被点击按钮的对象,由于这次被点击的是一个按钮,因此我们需要使用JButton对e.getSource()的返回值进行强转,随后通过getName()方法得到这个按钮的名字。至此整个点击事件处理完了。

2007年09月19日

进行测试为先测试驱动的程序设计是确保敏捷开发顺进行的有效措施。这篇案例将为读者提供详细的开发历程,来分析测试为先测试驱动的程序设计的过程。本文的重点:

   简要重复叙述一下测试为先/测试驱动得好处。
   简要介绍一下案例中的项目。
   没有利用测试为先/测试驱动设计的单元代码是什么样的?
   没有利用测试为先/测试驱动设计的单元代码里有什么样的问题
   针对单元代码设计的手动单元测试
   查找出毛病后的改进代码。

测试为先/测试驱动得好处

  传统的瀑布型软件开发是先从客户那里获得需求,然后进行纸上谈兵的设计,接着是程序源码写作构建,最后才是测试者对质量进行检评。从需求的分析到最后的测试,两者的相隔往往有好几个月。等到测试发现结构性问题时,重新设计已经成为一个无法完成的任务。设计者程序员已经无法回到几个月前推翻纸上谈兵的错误设计,重新用新的方式进行代码编写组合。测试为先/测试驱动和瀑布型软件开发不同是:

    测试模拟用户的使用组件的应用方式,为开发者提供解决方案
    测试驱使开发者开发可以测试的部件;
    及早测试,尽快排除设计中的各种微小问题;
    测试为开发者提供质保底线,每次的部件更改都能利用测试来检测修改后质量。

  以上这些都是我以前重复过的。这些说的容易但是想像起来是比较难一点。我下面所要谈到的案例并不是教科书里的完美案例,所谓的完美案例是完全可以自动化,完全可以进行单元测试的程序组件。举个例子说,想像你要设计一个类来代表“复数”(imaginary number),这样一个类是可以完全进行自动化单元测试。这种情况只能算得上百分之五十的现实情况,在其他百分之五十的状况下,一些手动测试和一些自动化测试都是必要的。还有很多情况下,手动测试是唯一的选择。半自动和手动测试并不代表整个开发不算作测试驱动开发。测试驱动的多数人都会说手动测试和半自动化测试并不能代表团队在进行测试驱动的开法。我觉得这种说法是偏见,只要测试组和开发组能够配合,尽可能地在最早时间将用户需求确定后,让测试组开始针对用户需求,设计思路进行测试用例设计,开发和测试能同时进行,开发出的部件能够迅速进行测试,测试用例能够经常地运行确保开发的质量不受变化的影响。这就是测试为先/测试驱动的开发。

本文的案例简介

  用来演示测试为先/测试驱动的开发,我将使用我最近设计的一个将应用程序图标加入System Tray里的类。然后在应用程序退出后,自动将图标从System Tray里删除。这样的类,你如果知道Windows系统对System Tray里的图标管理,就知道设计这么一个类的自动化测试并不简单。我觉得这种和图形界面打交道的类,也没有必要100%地进行自动化测试。所以我对这个类的测试驱动采取手工测试为主的测试,以测试者甚至开法者本身用用户的需求,先用例程作为基础,来设计图标管理类的单元测试。

案例的用户需求

  我是这个类的唯一用户,对于我要设计的程序,我的使用是很简单的。下面的列表就是我的需求:

   System Tray里的应用图标的数据管理和图标的加入删除都由类对象来进行;
   类对象能够设定视窗柄;
   类对象能够设定图标的独特ID;
   类对象能够设定图标对系统信息处理的消息ID;
   类对象能够设定图标在鼠标指向后能够显示有关程序的信息(程序的名称,设计公司和其他信息);
   类对象必须在调用者的指示下将图标放入System Tray。
   类对象必须处理让调试者能够删除System Tray里的图标。
   类对象在自我摧毁的时候自动删除System Tray里的图标。

  这些用户需求就是我要设计使用案例,在敏捷中,这些案例就是一个个故事。我在设计每一个故事的编码之前就先设计一个测试案例。每个测试案例都在设计完成之前会运行失败。设计完成后,这些测试案例才能顺利运行。

  为了调试图标的加入和删除都能正确运行,我决定使用一个简单的Win32视窗程序来作为我的单元测试温床,我的测试是手动测试。我的目标是用单元测试来尽可能地覆盖我设计的代码面积。第一步我设计了以下的单元测试:

void UnitTestCase0(HWND hWnd, HICON handleIcon)
{
// a normal core functionality test.
gSysTrayIcon.SetTrayIconID(11200);
gSysTrayIcon.SetNotifyWindow(hWnd);
gSysTrayIcon.SetTrayIcon(handleIcon);
gSysTrayIcon.SetTrayIconTip(_T("SysTrayIcon"));
gSysTrayIcon.SetTrayIconWmMsg(WM_TRAYICON_MSGS);
if (!gSysTrayIcon.AddIconToSysTray())
{
::MessageBox(hWnd, _T("Unable to add Icon to System Tray."), _T("Error:"), MB_OK);
return;
}
}

  这是一个很简单的函数,我假设我有一个全局变量叫gSysTrayIcon。它是一个类对象;它有至少六个函数;它的五个函数是数据设定函数;它的最后一个函数是让调用者告诉它把图像加入System Tray。根据我自己设计的单元测试案例,我设计了以下的类:

#ifndef SYS_TRAY_ICON_H_
#define SYS_TRAY_ICON_H_

#include "shellapi.h"

class SysTrayIcon
{
private:
NOTIFYICONDATA niData;

public:
SysTrayIcon();
~SysTrayIcon();

void SetTrayIconID(UINT iconID);
void SetNotifyWindow(HWND hWnd);
void SetTrayIcon(HICON iconHandle);
void SetTrayIconTip(LPCTSTR szMsg);
void SetTrayIconWmMsg(UINT wmMsg);

BOOL AddIconToSysTray();
BOOL DeleteIconFromSysTray();

};

#endif

  我的类成员设计如下,这里面有很多我无意中犯下的错误,也有我故意设置的错误,后面我用单元测试一点点地查找出一些常见的问题。为了顺利通过我上面的单元测试,首先看看我的设计初稿: #include "StdAfx.h"

#include "SysTrayIcon.h"
#include <string.h>

SysTrayIcon::SysTrayIcon()
{
ZeroMemory(&niData, sizeof(NOTIFYICONDATA));
niData.cbSize = (DWORD)sizeof(NOTIFYICONDATA);
niData.uFlags = NIF_ICON|NIF_MESSAGE|NIF_TIP;
}

SysTrayIcon::~SysTrayIcon()
{
DeleteIconFromSysTray();
}

void SysTrayIcon::SetTrayIconID(UINT iconID)
{
niData.uID = iconID;
}

void SysTrayIcon::SetNotifyWindow(HWND hWnd)
{
niData.hWnd = hWnd;
}

void SysTrayIcon::SetTrayIcon(HICON iconHandle)
{
niData.hIcon = iconHandle;
}

void SysTrayIcon::SetTrayIconWmMsg(UINT wmMsg)
{
niData.uCallbackMessage = wmMsg;
}

void SysTrayIcon::SetTrayIconTip(LPCTSTR szMsg)
{
_tcscpy(niData.szTip, szMsg);
}

BOOL SysTrayIcon::AddIconToSysTray()
{
Shell_NotifyIcon(NIM_ADD, &niData);
return TRUE;
}

BOOL SysTrayIcon::DeleteIconFromSysTray()
{
return Shell_NotifyIcon(NIM_DELETE, &niData);
}

  敏捷的宗旨是,在最短的时间内为客户提供完整的设计,让客户能够看到期待的价值,让客户能迅速反馈,并把反馈意见转变为设计改进。我以上的代码给我自己提供一个可以测试的机会。我用我的测试案例来实践我的设计,测试程序是一个SDI视窗程序。程序运行开始先把一个图标放入System Tray,然后,用户可以按在程序的缩小按钮上,程序会消失,但是System Tray里的程序图标。用户用鼠标左键双击System Tray里的程序图标,程序视窗会重新出现在桌面上。用户把鼠标光标移到System Tray里的程序图标上,一秒钟后就会一个提示标题出现,显示程序的名称。当我关闭程序视窗,视窗消失,System Tray里的程序图标也一并消失。这就是我的第一个测试。这个测试案例运行,不会出现任何问题。

  我写的第一个案例是开发者通常会做的测试,一个简单的案例保证设计到达最基本的用户需求。作为认真的开发者,和有专业意识的QA,这样简单的测试根本不够。各种各样的边界问题会通过设计的空隙造成程序运行异常。我就设计了另一个测试边际问题的测试,代码如下:

void UnitTestCase1(HWND hWnd, HICON handleIcon)
{
gSysTrayIcon.AddIconToSysTray();
}

  这个案例其实很简单。假设我建立了一个gSysTrayIcon,但是我不对其做任何初始化设定。那会出现什么问题?我运行一下这个案例,结果我马上发现了两个问题,一是ystem Tray里的程序图标是一个空格。接着我把标光标移到System Tray里的程序图标的位置上,马上那个位置就被其他图标给占据了。这些行为都是不对的。

  仔细看看我的设计,我在调用AddIconToSysTray()之前,没有调用一些重要的对象处理,这三个:SetNotifyWindow(HWND hWnd),SetTrayIcon(HICON iconHandle),和SetTrayIconWmMsg(UINT wmMsg)。所以我的案例会出现异常。在现实中,测试或者开发者自己都能运用自己的经验和知识来判断这些边界的问题,然后用单元测试来鉴别设计在处理这些问题的能力。现在我已经了解到我的设计有毛病,就要想办法解决。首先看看SetNotifyWindow(HWND hWnd),这个函数的边界是HWND参数不能是NULL(或是0)。如果这种情况出现,我应该如何处理?我的解决是用扔出异常。我就要为以上的单元测试进行一点改变。下面是我的修改:

void UnitTestCase1(HWND hWnd, HICON handleIcon)
{
// without any initailization.
try
{
if (!gSysTrayIcon.AddIconToSysTray())
{
::MessageBox(hWnd, _T("Unable to add Icon to System Tray."), _T("Error:"), MB_OK);
return;
}
}
catch(const AppException& e)
{
::MessageBox(hWnd, e.ToString(), _T("Error:"), MB_OK);
}
}

  然后我再更改我的设计,促使我的设计在处理错误输入时会抛出异常:

void SysTrayIcon::SetNotifyWindow(HWND hWnd)
{
if (hWnd == NULL)
{
// throw excepttion
throw AppException(_T("The handle of the window is invalid."));
}
niData.hWnd = hWnd;
}

  我再运行一下我的案例,结果还是不行,原来的毛病一点都没有改变。我再看看我的测试案例,结果发现我的修改并没有解除我所面对的问题。在我调用AddIconToSysTray()之前,我根本没有调用SetNotifyWindow,所以我的测试案例根本没有解决我的问题。我要修改的是AddIconToSysTray()。下面是我的修改:

BOOL SysTrayIcon::AddIconToSysTray()
{
if (niData.hWnd == NULL)
{
throw AppException(_T("The handle of the window is invalid."));
}
else if (niData.hIcon == NULL)
{
throw AppException(_T("The handle of the icon is invalid."));
}
else if (niData.uCallbackMessage == 0)
{
throw AppException(_T("The callback message ID is invalid."));
}

BOOL retVal = Shell_NotifyIcon(NIM_ADD, &niData);
return retVal;
}

  修改后运行一下,我的程序输出了异常信息提示,当我选择提示的“OK”按钮后,程序没有在System Tray里添加程序图标。我为了测试剩下两个判断分支,设计了两个案例里,加上上一个案例我有三个:

void UnitTestCase1(HWND hWnd, HICON handleIcon)
{
// without any initailization.
try
{
if (!gSysTrayIcon.AddIconToSysTray())
{
::MessageBox(hWnd, _T("Unable to add Icon to System Tray."), _T("Error:"), MB_OK);
return;
}
}
catch(const AppException& e)
{
::MessageBox(hWnd, e.ToString(), _T("Error:"), MB_OK);
}
}

void UnitTestCase2(HWND hWnd, HICON handleIcon)
{
// without any initailization on ICON handle.
try
{
gSysTrayIcon.SetNotifyWindow(hWnd);
if (!gSysTrayIcon.AddIconToSysTray())
{
::MessageBox(hWnd, _T("Unable to add Icon to System Tray."), _T("Error:"), MB_OK);
return;
}
}
catch(const AppException& e)
{
::MessageBox(hWnd, e.ToString(), _T("Error:"), MB_OK);
}
}

void UnitTestCase3(HWND hWnd, HICON handleIcon)
{
// without any initailization for message callback ID.
try
{
gSysTrayIcon.SetNotifyWindow(hWnd);
gSysTrayIcon.SetTrayIcon(handleIcon);
if (!gSysTrayIcon.AddIconToSysTray())
{
::MessageBox(hWnd, _T("Unable to add Icon to System Tray."), _T("Error:"), MB_OK);
return;
}
}
catch(const AppException& e)
{
::MessageBox(hWnd, e.ToString(), _T("Error:"), MB_OK);
}
}

  还有什么可以测试?首先,NOTIFYICONDATA::uID的值有没有限度?我们可以试试0和-1(-1应该是32位正值整数的最大值),对程序的影响也不大:

void UnitTestCase4(HWND hWnd, HICON handleIcon)
{
// What happen to have Icon ID to be 0.
gSysTrayIcon.SetTrayIconID(0);
gSysTrayIcon.SetNotifyWindow(hWnd);
gSysTrayIcon.SetTrayIcon(handleIcon);
gSysTrayIcon.SetTrayIconTip(_T("SysTrayIcon"));
gSysTrayIcon.SetTrayIconWmMsg(WM_TRAYICON_MSGS);
if (!gSysTrayIcon.AddIconToSysTray())
{
::MessageBox(hWnd, _T("Unable to add Icon to System Tray."), _T("Error:"), MB_OK);
return;
}
}

void UnitTestCase5(HWND hWnd, HICON handleIcon)
{
// What happen to have Icon ID to be 0.
gSysTrayIcon.SetTrayIconID(-1);
gSysTrayIcon.SetNotifyWindow(hWnd);
gSysTrayIcon.SetTrayIcon(handleIcon);
gSysTrayIcon.SetTrayIconTip(_T("SysTrayIcon"));
gSysTrayIcon.SetTrayIconWmMsg(WM_TRAYICON_MSGS);
if (!gSysTrayIcon.AddIconToSysTray())
{
::MessageBox(hWnd, _T("Unable to add Icon to System Tray."), _T("Error:"), MB_OK);
return;
}
}

  最后是鼠标滑到System Tray的图标时要显示的字符串。NOTIFYICONDATA::szTip最大容量应该是64个字节。我用以下的测试案例来测试我的设计:

void UnitTestCase6(HWND hWnd, HICON handleIcon)
{
// What happen to have Icon ID to be 0.
gSysTrayIcon.SetTrayIconID(11200);
gSysTrayIcon.SetNotifyWindow(hWnd);
gSysTrayIcon.SetTrayIcon(handleIcon);
gSysTrayIcon.SetTrayIconTip(_T("kdhfhdfjhdsfhdsjfhdsjhfjdshfjdshfjdshjfhdsjfsjdhfjdshjs" \
"hdfhdsfjhdsjfhsdjfhdshfhdsjfhdsfhsdjhfsdjhfjdshfjhdsfjhdsfhsdhfsjdhfjshdjfhdsfjhsdj" \
"dfhhdsjfhdjshfjdhfjdhfdhfjhdsfjhdjhfjdsfhjsadhhdskfhadskfhdskjfhkdsjfhkdsjhfkjadshf" \
"kjasdhkfhadskfhdskjhfkadsjhfkfashkfhaskdhfkadsfhkdsafhkdsjhfkdsahfkdshkjfhdaskhkads" \
"hfkdshfkjdhfkjdhfkdshfiohirhu’prhpiurhf"));
gSysTrayIcon.SetTrayIconWmMsg(WM_TRAYICON_MSGS);
if (!gSysTrayIcon.AddIconToSysTray())
{
::MessageBox(hWnd, _T("Unable to add Icon to System Tray."), _T("Error:"), MB_OK);
return;
}
}

  让我吃惊的是,程序并没有出现任何异常。只是64字节以后的字节都被忽略了。这种现象并不代表我的设计没有问题,我的原有设计是这样的:

void SysTrayIcon::SetTrayIconTip(LPCTSTR szMsg)
{
_tcscpy(niData.szTip, szMsg);
}

  _tcscpy是个很不安全的函数,它不检测接受缓冲的容量,所以,原缓冲的容量可以比接受缓冲的容量大,这样的代码很容易出现buffer overflow。所以,我们必须在字符串拷贝的时候检测两个缓冲的容量大小,接受缓冲的容量必须比原缓冲的容量要大。但是在我们现在所面对的情况下,这种情况我们采用另一种解决方式比较容易,就是保证字符串的拷贝不超过一定的数量。而且我们采用一个比较安全的拷贝函数。首先我改变了原有的测试用例:

void UnitTestCase6(HWND hWnd, HICON handleIcon)
{
// What happen to have Icon ID to be 0.
gSysTrayIcon.SetTrayIconID(11200);
gSysTrayIcon.SetNotifyWindow(hWnd);
gSysTrayIcon.SetTrayIcon(handleIcon);
try
{
gSysTrayIcon.SetTrayIconTip(_T("kdhfhdfjhdsfhdsjfhdsjhfjdshfjdshfjdshjfhdsjfsjdhfjdshjs" \
"hdfhdsfjhdsjfhsdjfhdshfhdsjfhdsfhsdjhfsdjhfjdshfjhdsfjhdsfhsdhfsjdhfjshdjfhdsfjhsdj" \
"dfhhdsjfhdjshfjdhfjdhfdhfjhdsfjhdjhfjdsfhjsadhhdskfhadskfhdskjfhkdsjfhkdsjhfkjadshf" \
"kjasdhkfhadskfhdskjhfkadsjhfkfashkfhaskdhfkadsfhkdsafhkdsjhfkdsahfkdshkjfhdaskhkads" \
"hfkdshfkjdhfkjdhfkdshfiohirhu’prhpiurhf"));
}
catch(const AppException& e)
{
::MessageBox(hWnd, e.ToString(), _T("Error:"), MB_OK);
}
gSysTrayIcon.SetTrayIconWmMsg(WM_TRAYICON_MSGS);
if (!gSysTrayIcon.AddIconToSysTray())
{
::MessageBox(hWnd, _T("Unable to add Icon to System Tray."), _T("Error:"), MB_OK);
return;
}
}

  用我原来的设计来测试以上的测试用例,什么都不会发生。说明我的原有设计要改变一些:

void SysTrayIcon::SetTrayIconTip(LPCTSTR szMsg)
{
HRESULT hr = StringCchCopyN(niData.szTip, 63, szMsg, 63);
if (FAILED(hr))
{
// throw exception
throw AppException(_T("Invalid tip string"));
}
}

  使用更新的代码,再运行我的测试用例,让我意外的是,测试用例竟然报道程序出错。说明我的更改是正确的。我的希望是如果原缓冲的容量太大,程序只拷贝接受缓冲容量所能承受的字符串量。这样我的测试用例应该不会报错。是什么造成这个问题?仔细查询一下有关StringCchCopyN()的说明,就发现我的问题在哪里了。如果原缓冲的容量太大,程序只拷贝接受缓冲容量所能承受的字符串量,StringCchCopyN()的返回值是STRSAFE_E_INSUFFICIENT_BUFFER,而不是S_OK。所以我的源代码必须进行一定的变化:

void SysTrayIcon::SetTrayIconTip(LPCTSTR szMsg)
{
HRESULT hr = StringCchCopyN(niData.szTip, 63, szMsg, 63);
if (FAILED(hr))
{
if (hr != STRSAFE_E_INSUFFICIENT_BUFFER)
{
// throw exception
throw AppException(_T("Invalid tip string"));
}
}
}

  再次运行上面的测试用例,我不再看见原有的错误消息。你可以看出我到现在,一直在使用我的测试和我对我要设计的代码的结构的熟悉来指导我的设计。白盒测试不仅能提供我所需要的程序质量检测,同时也指引我的设计方向。再试试几个其他的案例,这就是一个典型的案例,如果我用NULL作为原缓冲,那会出现什么问题:

void UnitTestCase8(HWND hWnd, HICON handleIcon)
{
// What happen to have Icon ID to be 0.
gSysTrayIcon.SetTrayIconID(11200);
gSysTrayIcon.SetNotifyWindow(hWnd);
gSysTrayIcon.SetTrayIcon(handleIcon);
gSysTrayIcon.SetTrayIconTip(NULL);
gSysTrayIcon.SetTrayIconWmMsg(WM_TRAYICON_MSGS);
if (!gSysTrayIcon.AddIconToSysTray())
{
::MessageBox(hWnd, _T("Unable to add Icon to System Tray."), _T("Error:"), MB_OK);
return;
}
}

  试试之后的结果是整个测试程序垮掉。我们又发现了一个问题。我先更改一下我的测试用例:

void UnitTestCase8(HWND hWnd, HICON handleIcon)
{
// What happen to have Icon ID to be 0.
gSysTrayIcon.SetTrayIconID(11200);
gSysTrayIcon.SetNotifyWindow(hWnd);
gSysTrayIcon.SetTrayIcon(handleIcon);
try
{
gSysTrayIcon.SetTrayIconTip(NULL);
}
catch(const AppException& e)
{
::MessageBox(hWnd, e.ToString(), _T("Error:"), MB_OK);
}
gSysTrayIcon.SetTrayIconWmMsg(WM_TRAYICON_MSGS);
if (!gSysTrayIcon.AddIconToSysTray())
{
::MessageBox(hWnd, _T("Unable to add Icon to System Tray."), _T("Error:"), MB_OK);
return;
}
}

  然后再修改我的设计:

void SysTrayIcon::SetTrayIconTip(LPCTSTR szMsg)
{
if (szMsg == NULL)
{
throw AppException(_T("Tip string pointer cannot be NULL."));
}

HRESULT hr = StringCchCopyN(niData.szTip, 63, szMsg, 63);
if (FAILED(hr))
{
if (hr != STRSAFE_E_INSUFFICIENT_BUFFER)
{
// throw exception
throw AppException(_T("Invalid tip string"));
}
}
}

  运行后看到我所希望看到的错误信息。这样我又防止了一个毛病漏到测试组的手上。

  最后在总结之前,说说图标柄输入如果是NULL的情况,我们可以进行一些改进,如果用户在调用SysTrayIcon::SetTrayIcon(HICON iconHandle)输入非法值NULL,解决方法不一定是要直接抛出异常。我们可以让系统帮助设定一个默认图标柄。首先我们再Copy & paste制作一个新的单元测试:

void UnitTestCase9(HWND hWnd, HICON handleIcon)
{
// What happen to have Icon ID to be 0.
gSysTrayIcon.SetTrayIconID(15923);
gSysTrayIcon.SetNotifyWindow(hWnd);
gSysTrayIcon.SetTrayIcon(NULL);
gSysTrayIcon.SetTrayIconTip(_T("SysTrayIcon"));
gSysTrayIcon.SetTrayIconWmMsg(WM_TRAYICON_MSGS);
if (!gSysTrayIcon.AddIconToSysTray())
{
::MessageBox(hWnd, _T("Unable to add Icon to System Tray."), _T("Error:"), MB_OK);
return;
}
}

  运行以上的单元测试,我马上看见原有的异常被抛出,表明我的测试失败了。我的意图是如果图标柄的设置是NULL,那么,我就让系统找到一个默认的图标,并用它作为程序在SystemTray里的图标。我对代码进行以下修改:

BOOL SysTrayIcon::AddIconToSysTray()
{
if (niData.hWnd == NULL)
{
throw AppException(_T("The handle of the window is invalid."));
}
else if (niData.hIcon == NULL)
{
HICON defaultIcoHdl = ::LoadIcon(NULL, MAKEINTRESOURCE(IDI_APPLICATION));
if (defaultIcoHdl != NULL)
{
niData.hIcon = defaultIcoHdl;
}
else
{
throw AppException(_T("The handle of the icon is invalid."));
}
}
else if (niData.uCallbackMessage == 0)
{
throw AppException(_T("The callback message ID is invalid."));
}

BOOL retVal = Shell_NotifyIcon(NIM_ADD, &niData);
return retVal;
}

  运行我的单元测试,结果就没有出错,但是我的第二个单元测试原来设计为,如果图标柄的设置是NULL,就抛出异常,现在这个使用案例已经没有意义了,所以第二个单元测试就可以被删掉了。

总结

  我的这篇文章完整(并非完美)地展示了一个简单的测试为先,测试驱动的开发案例。设计过程中,我用测试案例来主导我的设计,只有最简单的设计来实现我的需求。我只在更改错误的情况下增加功能,而不是随便凭着自己的想像来增加我不需要的功能。我在测试中找出了不少我问题,而且都是在开发过程中发现的问题,也就是说在开法的最基本阶段测试就开始进行了,而且很多问题在开发初期就被检测出来并修改好,尽早测试,可以为后面的开发减少很多不必要的困难。

  我这个案例不是一个完美的案例。现实中,能够完美展示单元测试的好处的完美案例是不存在的,所有我见过的完美案例都是在教科书里出现的。这些案例有时给人的感觉是不真实,也展示出这些案例的局限性。我的案例在很大程度上依赖手动化测试,这有时是违反敏捷开发的用意的。在敏捷开发中,自动化单元测试和接受性测试是非常重要的。我敢说很多进行敏捷开发的专家都会说我的案例算不上敏捷开发。我对这一观点只同意到一定的程度,软件开发是个人与人互动的社会活动,开发者在手动测试上所花时间过多的话,就要将这样的任务推给QA,有能力的QA应该可以和开发者一起考虑什么样的接受性测试和单元测试能够帮助整个团队提交更好的产品。

  我这个案例同时也说明,用户界面的设计也能通过单元测试来进行。这样的测试不仅仅是开发者自己进行,有能力的QA可以和开发者一起进行接受性测试,QA可以享受一下开发的乐趣。同时可以和开发者一起合作进行质量监控,这样双方不会因为竞争而感受双方的相互威胁,QA帮助开发者及早进行测试,从而建立友好的合作关系。