设计复合应用程序:单元测试
 

2009-03-27 作者:Craig Wolpert,Jo Grant 来源:IBM

 
本文内容包括:
为了方便用户将多个组件组装成 Lotus Notes 复合应用程序,需要在使用组件之前先对其进行单元测试。本文是“设计复合应用程序”系列文章的第四篇,将介绍在组装复合应用程序之前如何对组件进行测试。

编辑注:本文是有关设计复合应用程序系列文章的第四篇,该系列文章将在随后几个月内在 developerWorks Lotus 上发表。参见前几篇 developerWorks 文章:“设计复合应用程序:设计模式”、“设计复合应用程序:组件设计” 和 “IBM Lotus Notes V8 中的 Lead Manager 应用程序:概览”。

关于本系列

复合应用程序是面向服务体系结构(Service Oriented Architecture,SOA)和上下文协作策略中的关键元素。它们为公司和组织提供业务灵活性,使他们能够快速响应市场的需求变化。复合应用程序由松散耦合的用户界面组件组成,支持组件之间的通信。

组件可以在多个复合应用程序中重用。将多种技术组合成一个应用程序的能力可以提供巨大的业务价值。它可以保护和扩展公司现有的软件资产,增加业务灵活性。与传统的应用程序开发方式相比,创建复合应用程序要容易得多,这帮助公司快速且成本高效地响应业务需求。

复合应用程序体系结构的这种松散耦合特性还使位于不同地点的小组可以相互分享工作成果并进行协作。每个小组只需要了解组件的输入和输出,而不必掌握其内部逻辑。例如,一个部门正在开发会计应用程序,另一个小组正在开发销售趋势跟踪应用程序。在复合应用程序体系结构中,可以将销售趋势跟踪应用程序中的一些组件添加到会计应用程序中,从而按照会计学的上下文提供相关的资产视图。同样地,在销售趋势跟踪应用程序中也可以添加会计应用程序中的组件,从而按照资产的上下文提供会计信息。随着开发出的复合应用程序越来越多,在整个公司范围内共享组件的机会会呈指数级增长。这种体系结构的目的就是让总体大于部分之和。

复合应用程序模型是 IBM WebSphere Portal 开发人员已经熟悉的模型。这种方式已经扩展到了 IBM Lotus Notes V8 之中,这让 Lotus Notes 开发人员能够在复合应用程序中以组件形式提供他们的 Lotus Notes 应用程序。IBM Lotus Domino Designer V8 进行了进一步扩展,可以使用属性代理并提供一个更直观的用户环境。Lotus Notes V8 还支持插入 Eclipse 组件。复合应用程序可以是 Lotus Notes 组件和 Eclipse 组件的任意组合。这适合进行 on-the-glass 集成(将不同组件集成在同一用户界面中);如果进行扩展,还可以通过属性代理让组件可以相互操作。可以使用 Composite Application Editor 或 WebSphere Portal Application Template Editor 定义复合应用程序。

复合应用程序的组件开发不同于传统的 Eclipse 或 Lotus Notes 应用程序开发。这种开发的目标是创建具有足够灵活性的组件,让它们可以简便地部署在应用程序中,而不需要做重大修改。

前提条件

本文假设您熟悉 Lotus Notes 中的复合应用程序,因此,重新温习 IBM Lotus Domino Designer 帮助 中的复合应用程序内容将有所帮助。

您应该重读 developerWorks Lotus 文章 “设计复合应用程序:设计模式”、“设计复合应用程序:组件设计” 和 “IBM Lotus Notes V8 中的 Lead Manager 应用程序:概览”,这些文章从较高的层次介绍了组件设计,讨论了以领域为中心的组件和上下文领域组件,这些组件本身都是元模式。

单元测试

复合应用程序的创建与传统应用程序开发的一个重要区别是:复合应用程序的开发工作分为两个阶段。首先,编写组件,然后将组件组装为应用程序。这种方法具有几个优点。由于可以通过多种方式重用组件,因此可以通过 “一次构建,多次使用” 的方式优化开发。其次,组装组件不像构建组件那样具有较高的技术含量,因此各种人员均可胜任这项任务。因此,应用程序开发更贴近最终用户,这使得应用程序设计更注重领域专家知识。

尽管具备这样的优点,仍然需要解决一些问题。因为组件并不是针对特定用途定制,必须尝试考虑到所有场景。由于复合应用程序在后期由具有一般技术的人员组装,因此组件的行为必须一致并且可预测。组件应该具备良好的文档,描述传递给组件接口的有效值。

确保成功实现组件开发和部署的一个重要方法就是单元测试。如果能够在部署之前保证组件可以正常工作,那么在组装组件时,就不会产生大量的问题和挫折。从而提供更好、更经济有效的体验。

常见方法

由于组件不能在代码级别进行互操作,因此不适合使用传统的单元测试方法。当然,如果组件仅仅提供较低级别的业务逻辑,则最好通过编程方式测试业务逻辑。LotusScript 库可以为 Lotus Notes 代码实现这一任务,而 Eclipse 的 JUnit 模块则适合用于 Java 代码。但是对于组件本身,最好的测试方法位于组件构建所针对的环境的内部,即复合应用程序。

本文介绍的关键方法主要围绕一个 test 组件的创建和使用。非常简单,这个组件具有任何组件开发中使用到的各种数据类型的属性和操作。这些属性和操作均成对出现并与 UI 连接起来。当对 test 组件设置一个操作时,相应的 UI 部分将填充所接收的值。该值可由操作员修改或编辑,然后可以按下 Set 按钮,设置之后,Edit 字段的当前值将触发属性。

这个 test 组件随后将连同另一个(且只有一个)组件部署到复合应用程序中,这个组件就是将要被测试的组件。被测试的组件发布的所有属性将被连接到 test 组件中与该属性类型对应的操作。被测试的组件所消费的操作在 test 组件上都有一个对应类型的属性相连接。这样,每当被测试的组件修改属性值时,操作员都可以立即觉察到。操作员还可以在 test 组件中填充值并单击 Set 按钮,从而检验被测试组件中的任何操作。

现在,可以编写一个测试脚本并与测试应用程序一起运行,以检验测试中的组件。脚本包含了在进行测试的组件中执行操作的说明以及在 test 组件中获得的预期结果,从而跟踪所触发的属性。还包含了有关针对操作输入的适当值的说明以及在 UI 中获得的预期反应。通过这种方式,可以测试组件的所有功能、声明有效的行为、检验边界条件并确保适当地将组件降级。

在某些情况下,在一个单元测试复合应用程序中测试多个组件更可取。在这种情况下,应该首先单独测试各个组件,然后将组件组合在一起执行综合测试。

开发 test 组件

test 组件可以在 Eclipse 中实现,也可以在 Lotus Notes 中实现,本文将对这两种情况进行讨论。因为组件在属性代理级别通信,因此具体的实现位置并不重要,选择您最熟悉的体系结构即可。

第一步对于两种情况下的组件是通用的。需要创建一个 Web Services Description Language (WSDL) 文件声明 test 组件要使用的属性和操作。首先,调查使用的其他组件。收集它们使用的名称空间和数据类型。进行规划,对每个惟一的类型至少提供一个属性和一个操作。如果某个类型具有大量属性,或者在您设想的测试用例中,希望能够区分单个类型的多种操作或属性,那么在 test 组件中创建多个条目。

如果检验标准 Lotus Notes 组件声明的属性和操作,将会发现如下类型:

名称空间:http://com.ibm.propertybroker.standardtypes

类型:

  • emailAddress
  • canonicalName
  • commonName
  • NotesURL
  • MailTo
  • toField

名称空间:http://w3.ibm.com/xmlns/ibmww/sw/datatype

类型:

  • secondaryDateList

名称空间:http://www.w3.org/2001/XMLSchema

类型:

  • string

注意:还有一些类型基于 datetime 格式,但是本文仅讨论简单的字符串类型。

基于此目的,我们为每个定制类型创建一个属性和操作,并且为通用字符串类型 Misc1、Misc2 和 Misc3 创建三个属性和操作。注意,所有属性名都使用 get 作为前缀,例如 getEmailAddress,而所有操作使用 set 作为前缀,例如 setEmailAddress。

创建 WSDL 文件的方式有很多,可以使用 Lotus Notes 附带的 Property Broker Editor、Eclipse 附带的 WSDL Editor、IBM Rational Application Developer for WebSphere Software,或手工创建。一种方法是首先从其中一个组件创建 WSDL 文件,然后不断进行扩展,直到包含所有必需的类型,然后使用新内容替换属性和操作。

下载小节提供了本文使用的 WSDL 文件。

开发 Lotus Notes test 组件

在 Lotus Notes 中创建 test 组件的最简单的方法是创建一个 Lotus Notes 组件(数据库),其中包含一个 Lotus Notes 组件视图。我们根据一个空白模板创建一个 STTester.nsf 文件,并且为了方便访问,将常见的属性代理例程导入到 LotusScript 库中。这些例程的实现包含在 PBRoutines Script Library 中的 Lotus Sandbox 示例 IBM Lotus Notes 8 中的 Lead Manager 复合应用程序示例 中。随后再添加一个名为 ST Tester 的表单。

在表单的主区域中创建一个表,使用一行用于各个已定义的属性和操作对,一列用于标签,另一列用于 Edit 字段,最后一列用于 Set 按钮。字段根据它们对应的基属性操作命名。在本例中,我们使用 EmailAddress 等名称。

我们将每个 Set 按钮设置为向属性代理发布与之对应的字段的值。清单 1 所示的简单脚本将执行这一操作。

清单 1. Set 按钮代码
 
				
Sub Click(Source As Button)
	Const PropName = "EmailAddress"
	Dim workspace As New NotesUIWorkspace
	Dim uidoc As NotesUIDocument
	Dim PropValue As String
	Set uidoc = workspace.CurrentDocument
	PropValue = uidoc.FieldGetText( PropName )
	publishProperty "get"+PropName, PropValue
End Sub

因为我们打算将这个脚本剪贴到每个 Set 按钮中,并且脚本之间的惟一区别就是属性名,因此将在脚本的开始部分的一个常量中声明。粘贴脚本后,只需修改这一处位置。脚本的其余部分十分简单;我们在文档中获得字段的当前值,然后将它发布给属性代理。

我们针对每一个值定义了一个 Lotus Notes 操作。在 Lotus Notes 操作的属性框中,我们将它映射到 WSDL 文件中定义的对应的属性代理操作。操作体是一个简单的脚本,如清单 2 所示。

清单 2. 定义一个 Lotus Notes 操作
 
				
Sub Click(Source As Button)
	Const PropName = "EmailAddress"
	Dim workspace As New NotesUIWorkspace
	Dim uidoc As NotesUIDocument
	Dim PropValue As String
	Set uidoc = workspace.CurrentDocument
	PropValue = getActionParameter()
	uidoc.FieldSetText PropName, PropValue
End Sub

这个脚本的惟一不同在于,我们接收了由属性代理操作传入的值并将它分配给它所对应的字段。最后,建议您创建一个不可见的 SaveOptions 计算式文本字段,将其值设置为 0。选择 Create - Field,向表单添加一个名为 SaveOptions 的文本字段,如下所示:

  1. 使用字段的属性框指明这是一个计算式字段。
  2. 为 SaveOptions 字段选择 programmers 面板,将值设为 “0”。确保使用引号括起字符 0。
  3. 在 work 面板中单击 SaveOptions,然后双击打开该字段的属性框。
  4. 在属性框的第 6 个附签(窗口阴影图形)中,在 selection 字段的 Hide 部分中,选择 Notes R4.6 或更高版本。

这将阻止系统在关闭表单时提示您保存表单。完成设置后,就可以将这个组件添加到您的面板,继而添加到复合应用程序中。

注意:在此强烈建议为您的单元测试组件(见图 1)创建一个单元测试。这似乎有些多余,但是,和对要测试的组件应用单元测试一样,也要对测试工具应用单元测试。

图 1. STTester Form 组件

开发一个 Eclipse test 组件

Eclipse test 组件 Standard Types Tester(参见图 2)的源代码可以在 OpenNTF.Org Web 站点获得,尝试这个典型的基于 Eclipse 的组件的代码,Component Library 中的大量组件都使用了这些代码。源代码被作为附件包含在 Help - About this Application 中。创建了一个插件来存放组件,添加了 WSDL 文件,并且创建了基本的视图类、数据模型和操作处理程序,并在扩展点得到引用。

为此,在数据模型中,为每个属性/操作对创建一个字段。然后通过 set 和 get 存取器函数公开这些字段,确保 set 函数将其更改发布到属性更改支持机制中。通过这种方式,helper 库可以确定何时发布属性。

图 2. Standard Types Tester

在创建视图时,对于成对的属性和操作,通常希望将信息链接起来。在设置了某个操作并调用了与之对应的属性后,允许以非常灵活的方式连接组件。但是,在本例中,我们需要特意区分组件接收和发送属性的方式。否则,无法保证实现传播,也不可能确切地了解哪些因素对于测试非常重要。

在这里,我们为视图创建了两个数据模型。其中一个连接到即将接收的操作,另一个连接到已经发送的属性。我们将一个侦听程序连接到传入的数据模型,当它侦听到一个修改时,将从数据模型中读取值并将值写入到 UI 字段中。另一方面,我们将一个侦听程序连接到 Set 按钮。当按下这个按钮时,它将读取 UI 字段的当前值并将其写入输出数据模型。通过库链接的方式可以将属性修改传播出去。

对于完整的组件,请参考 OpenNTF.org 上的复合应用程序组件库。以下是摘取的一些代码片断,展示了与标准模型的区别。首先,我们将声明两个数据模型(而不是一个),如清单 3 所示。

清单 3. 声明两个数据模型
 
				
    private STTesterViewBean    mDataIn;
    private STTesterViewBean    mDataOut;
    
    public STTesterView() {
        mDataIn = new STTesterViewBean();
        mDataOut = new STTesterViewBean();
    }

输出模型通过 PBBroadcast() helper 对象注册。这可以确保它所发生的修改将作为属性修改传播:

new PBBroadcast(this, mDataOut, "http://com.ibm.propertybroker.standardtypes,
http://w3.ibm.com/xmlns/ibmww/sw/datatype,http://www.w3.org/2001/XMLSchema");

输入模型在 getData() 方法中得到引用(helper 库中的抽象 IDataProvider 接口需要使用该方法),确保接收到的操作被写入输入数据模型:

public PCSBean getData() {
return mDataIn;
}

创建一个 helper 函数,用于向显示添加每一行,如清单 4 所示。

清单 4. 添加 helper 函数
 
				
    	addLine(client, "EmailAddress");
	...
    private void addLine(Composite client, String name) {
        GridUtils.makeLabel(client, makeLabelName(name), "");
        Text input = GridUtils.makeText(client, (String)null, "fill=h");
        mDataIn.addPropertyChangeListener(makePropertyName(name), new DoInput(input));
        Button go = GridUtils.makeButton(client, "Set", "");
        go.addSelectionListener(new DoOutput(input, name));
    }

这个 helper 函数为每个属性/值对创建了标签、输入字段和 Set 按钮。它还创建了所需的侦听程序侦听输入数据模型并将其中发生的修改传播到 UI 字段中。此外,它还创建了另一个侦听程序侦听 Set 按钮,当单击该按钮时,将把 UI 字段的值发布给输出数据模型,如清单 5 所示。

清单 5. 创建侦听程序
 
				
class DoInput implements PropertyChangeListener {
    ...
    public void propertyChange(PropertyChangeEvent ev) {
        mControl.setText(ev.getNewValue().toString());			
    }
}

class DoOutput implements SelectionListener {
    ...
    public void widgetSelected(SelectionEvent ev) {
        String newVal = mControl.getText();
        PBHandler.setValue(mDataOut, "set"+mProperty, newVal);			
    }
}

完成 Lotus Notes 实现后,可以将这个组件添加到面板,随后为其创建一个单元测试。

创建单元测试计划

每个组件都应具备一份规范,详细说明它在哪些条件下发布哪些属性、将消费哪些操作,针对这些操作的有效值以及在消费这类操作时将执行的任务。developerWorks 文章 “设计复合应用程序:组件设计” 针对这一内容进行了讨论。一份优秀的测试计划将解决并验证所有这些方面。如果具备了良好的规范,那么就很容易编写出出色的测试计划。

测试器组件在验证测试计划方面十分有帮助,但是,组件通常要与一组其他组件一同部署并进行链接以传播业务逻辑,这并不利于形成良好的测试环境。您无法创建特定的环境,并且由于每次要同时测试多项内容,因此很难判断哪些部分出现了问题。

相反,我们建议创建一个简单的复合应用程序,其中只包含要进行测试的组件和一个测试器组件。将进行测试的组件中的每个属性连接到 test 组件中的有效操作。对于进行测试的组件中的每个有效操作,确保从测试器组件中创建一个到它的连接。一旦对各个组件进行了单元测试,就很容易在应用程序中组装和连接这些组件。test 组件并不会一成不变,在组件发生修改时也应同时更新 test 组件。

对于 Lotus Notes 组件视图,属性和操作属于组件(数据库)范围,而不是组件视图(表单、框架集或视图),因此属性和操作可能出现在对该组件无效的位置。组件视图规范应该说明哪些属性和操作对于组件是有效的。由于用户可能进行错误的连接,因此应当考虑对属性和操作执行有效性测试,从而确保能够恰当地对组件进行降级,同时不会出现意料之外的副作用。

测试计划将按照如下所示的清单逐一进行测试:

  1. 正向测试
     
    • UI 动作(UI gesture)。对于规范中定义的各个 UI 动作,在单元测试中创建一行说明。要求操作员执行这个动作,然后在 test 组件中检验结果。是否发布了预期的属性?
    • 操作处理。对于每个受支持的操作,要求操作员在 test 组件中填充值,然后单击 Set 按钮。UI 是否发生了预期的修改?如果这一操作被设计为进一步触发属性,是否按预期发生?
  2. 反向测试
     
    • UI 动作。考虑用户对 UI 可能执行的所有动作,例如单击错误的位置或输入无效的值、重音字符或不常见的字符。为每个操作创建一行说明。要求操作员执行这些动作,判断不应该触发的属性是否被触发。
    • 操作处理。对于每个受支持的操作,设计所有错误的填充方法,例如空格、很长的字符串、格式错误的数据、重音字符或不常见的字符。为每种情况创建一行说明,并配有可剪贴到 UI 的样例文本。单击 Set 按钮,查看组件行为是否正确。
    • 操作处理。对于每个不受支持的组件,使用各种各样的值填充并设置。确保组件能够忽略这些值。

注意:尽管可以创建这些测试复合应用程序并在测试周期中使用,您还可以在开发周期内动态创建它们。这不会占用太多的时间,并且它们有助于识别通过其他方法很难发觉的开发和发行问题

表 1 展示了一个测试计划示例,可以使用它测试 Lotus Notes Contacts 视图。您可以在工作时将注释记录在这份计划中。

表 1. 针对 Lotus Notes Contacts 视图的测试计划示例
 
测试
测试设置
创建一个新的复合应用程序 TestNotesContactView.nsf。
在 Composite Application Editor (CAE) 中,在顶部添加 NotesContactsView,并在底部添加 Standard Types Tester。
在 NotesContactView 组件中打开连接工具。
按照如下方式连接:
Notes URL changed - Notes URL
Common name changed - Misc 1
Email address changed - Misc 2
Street address changed - Misc 3
 
保存并在 Standard Types Tester 组件中打开连接工具。
确保 NotesContactView 组件中没有显示任何操作。
退出 CAE。
打开 names.nsf,确保加载了正确的数据集。关闭 names.nsf。
测试执行
打开 TestNotesContactView.nsf 应用程序,确保没有属性被触发并且显示最后选择的字母页面。
 
从左侧导航器中选择字母 W 并确保:
显示以 W 开头的姓
选择第一个 Mis Wajda
在测试器组件中,Misc1 是 Wajda, Mis
在测试器组件中,Misc 2 是 MisWajda_cnnew1@llama.ibm.com
 
 
从左侧导航器中选择字母 X,确保没有显示名称并且没有属性发生更改。
 
 

图 3 展示了将对其使用这个测试计划的 test 组件。

图 3. 示例 test 组件

超越单元测试

这个结构和系统超越了简单的组件验证。任何测试都不是完美的,并且会出现无法预料到的复杂情形。开发人员普遍感到头痛的是复杂应用程序中简单组件出现问题。然而,对于这样的系统,您可以检查日志文件并使用其他调试方式来尝试重新创建造成错误的组件输入。使用单元测试复合应用程序,可以将这些值重新提供给出错的组件。幸运的话,可以在单元测试中重新生成错误并进行诊断,而不需要加载完整的复杂环境。当然,重新生成错误后,将在单元测试脚本中记录下这些操作的说明,以便在将来的版本中实现回归。

结束语

本文为开发单元测试提供了坚实基础,并展示了对各个组件进行测试非常有益于复合应用程序。在开发 Lead Manager 复合应用程序示例时,我们已经开发并执行了单元测试,并且认为单元测试对于开发流程非常有用,因为它使人们更容易认识到复合应用程序中可重用组件的优势。

参考资料

学习 获得产品和技术 讨论

火龙果软件/UML软件工程组织致力于提高您的软件工程实践能力,我们不断地吸取业界的宝贵经验,向您提供经过数百家企业验证的有效的工程技术实践经验,同时关注最新的理论进展,帮助您“领跑您所在行业的软件世界”。
资源网站: UML软件工程组织