[原英文链接]:
Telligent Systems, Inc.
链接:http://msdn.microsoft.com/en-us/library/ms364064.aspx
简介
随着业务的革新和发展,运行它们的系统也需要进行更新。随业务的发展、革新以及与合作伙伴、客户和供应商的结合,这些系统将在复杂性方面持续扩增。
这种复杂性迫使 IT 的领导者们在开发过程中(即,在实现之前)确保质量。有一种方法可使开发人员减少进入 QA 环节的故障数量,即,针对自定义代码严格执行自动化单元测试。在开发过程中强制使用自动化单元测试可为团队成员提供有关如何使用自定义代码的示例(这些示例易于使用并自行记录)。
使用结构化、自动化单元测试面临的挑战之一是完成这些任务所需的代码总数。(测试代码需要使用大量代码!)代码生成的概念(简单定义为“创建软件的软件”)正随着时间的快速推移而逐渐深入到团队 IT 开发之中。有些人认为代码生成有助于缩短“推向市场”策略的时间,强制内部标准/协定,并促进开发过程。
Microsoft 认识到这一需要后提供了一个功能丰富、带有下一代开发平台 Visual Studio 2005 Team System (VSTS) 的代码生成引擎。本文提供针对单元测试代码生成的循序渐进的指导,并深入探讨如何在用例中使用。
重新思考单元测试
请考虑以下情况:您负责为公司生成下一代系统,同时您是较大的开发团队中的一员。您是 UI 开发人员,负责尽可能多地创建 Microsoft ASP.NET/Microsoft WinForms。您依赖“中间层”团队完成其中间层组件 — 这些组件用于执行数据库 CRUD (Create-Retrieve-Update-Delete) 以及与该系统中每个实体相关的业务规则。
经过几周的 UI 开发,您完成了窗体并且收到了中间层开发人员打算向您提交其类库的消息。表 1 提供一段对话示例,说明我们大多数人在开发过程中都会遇到的一些事情。
表 1. UI 开发人员和中间层开发人员间的示例对话 | |
中间层: |
“这些对象随时供您使用 — 为此,只需获取 OurSystemBL.dll 的最新版本。” |
UI: |
“谢谢。您有供我们查看文档吗?” |
中间层: |
“哈哈!是的,当然有!我们花了很多时间编写它!请查看 Design Document — 噢,请等一等,它还没有完成……(不久之后即可完成!)” |
UI: |
“您使用 XML 文档了吗?” |
中间层: |
“在构造函数中,但许多方法都不使用。” |
UI: |
“显示如何创建、执行并删除对象的示例代码,怎么样?” |
中间层: |
“我已经附加了一个示例 WinForms 应用程序(从我的工作区),它应该能够提供一些您所需的内容……,虽然它不在 Microsoft Visual SourceSafe 中。” |
在考虑如何进行这样有趣的项目之后,您打定了主意,决定检验中间层的单元测试套件。在深入钻研该代码之后,您注意到该窗体有两个未标记的文本框,以及三个标记为 button1、button2 和 button3 的按钮(幸运的话,它们将排列在窗体上)。
接下来,在查看与这些按钮相关的事件之后,您认识到这些代码都未经注释,并且数据变量都被命名为 x、y、z。如果幸运,您还会注意到 button1 和 button2 执行该对象的 Save() 方法,而 button3 执行 Delete() 方法。执行时,您会接收到很多 System.Exception 错误,这是因为遗漏了很多配置设置。
这显然是一个特例,我希望多数开发团队不要进行这一试验,下面让我们看一下该方案中“单元测试”遇到的问题:
• |
这种形式的单元测试代码不是结构化的:代码充斥到按钮单击事件中并且难以编译。 |
||||
• |
这种形式的单元测试代码记录得不太好。 |
||||
• |
这种形式的单元测试并不基于“已知”为好或坏的数据 — 它完全依赖于输入到那些未标记的文本框的内容。
|
||||
• |
实现的详细信息不易于在团队成员间进行传播。 |
输入自动化单元测试
xUnit 框架在 1998 年作为 eXtreme 编程的核心概念引入。它提出了一个有效的机制,有助于开发人员将结构化、有效且自动的单元测试添加常规开发活动中。从那以后,该框架演化为针对自动化单元测试框架的实际标准。
创建自动化单元测试的用例
简单说,自动化单元测试是:
• |
结构化的。 |
• |
自行记录的。 |
• |
自动且可重复的。 |
• |
基于已知数据。 |
• |
旨在测试积极和消极操作。 |
• |
非常适合跨不同计算机的测试实现。 |
• |
配置、实现和执行的示例。 |
xUnit 框架元素
表 2 分析 xUnit 框架以及对应于 Visual Studio 2005 Team System 的 Unit Testing Framework 等价物的基本概念。
表 2. 相应的 xUnit 框架和 VSTS Unit Testing Framework 概念 | ||
xUnit 框架概念 | VS 2005 等价物(参见下面的属性) | 描述 |
测试 |
TestMethod |
简单说,这些是您的测试。测试预期结果的逻辑,并报告未取得结果(如果有)。请将它看作您的“方法”。 |
测试装置 |
TestClass |
针对大量测试的一个逻辑分组。请将它看作您的“类”。 |
测试套件 |
测试列表 ** |
针对大量测试装置的一个逻辑分组。请将它看作您的“类库”。 注不需要一个属性。 |
测试运行器 |
VS 2005 VSTS Unit Testing Framework |
GUI/Console 应用程序负责发现、执行和报告测试结果。Visual Studio 2005 Team System 将作为本文的测试运行器。 |
测试装置示例
请考虑以下针对 BankAccount 类的类关系图,以及一个示例测试装置 (BankAccountTests.cs)。
图 1. BankAccount 类
示例测试装置: BankAccountTests.cs
Code
主单元测试概念 == 断言
用于该形式单元测试的主要概念是,自动化单元测试是基于“断言”的,即可定义为“事实或您相信为事实的内容”。从逻辑角度看,请考虑该语句“when I do {x}, I expect {y} as a result”。
这可以轻松地翻译为代码,方法是使用 Microsoft.VisualStudio.QualityTools.UnitTesting.Framework 命名空间中可用的三个“断言”类中的任一个:
Assert、StringAssert 和 CollectionAssert。主类 Assert 提供用于测试基础条件语句的断言。StringAssert 类自定义了在使用字符串变量时有用的断言。同样,CollectionAssert 类包括在使用对象集合时有用的断言方法。
表 3 显示可用于当前版本 Unit Testing Framework 的断言。
表 3. VSTS Unit Testing Framework 断言 | ||
Assert类 | StringAssert 类 | CollectionAssert 类 |
AreEqual() AreNotEqual() AreNotSame() AreSame() EqualsTests() Fail() GetHashCodeTests() Inconclusive() IsFalse() IsInstanceOfType() IsNotInstanceOfType() IsNotNull() IsNull() IsTrue() |
Contains() DoesNotMatch() EndsWith() Matches() StartsWith() |
AllItemsAreInstancesOfType() AllItemsAreNotNull() AllItemsAreUnique() AreEqual() AreEquivalent() AreNotEqual() AreNotEquivalent() Contains() DoesNotContain() IsNotSubsetOf() IsSubsetOf() |
这些自动化单元测试用什么运行?
正如前面提到的,xUnit 框架将“测试运行器”的概念定义为应用程序负责:
(a) 执行单元测试;
(b) 报告测试结果。
对于本文,包含 Visual Studio 2005 Team System (VSTS) 的 Unit Testing 引擎作为我们的“测试运行器”。
图 2 表示 BankAccountTests.cs 类的执行结果。
图 2. 测试结果窗格:单元测试执行结果
Microsoft Visual Studio 2005 使用源项目的代码模型动态填充该视图。
它基于该源代码中的自定义属性动态发现有关该测试套件的信息。
表 4 表示最常见的单元测试属性(以及执行的次序)。
表 4. 常见单元测试属性 | |
属性 | 描述 |
TestClass() |
该属性表示一个测试装置。 |
TestMethod() |
该属性表示一个测试用例。 |
AssemblyInitialize() |
在执行为执行选择的第一个 TestClass() 中的第一个 TestMethod() 之前,执行带有该属性的方法。 |
ClassInitialize() |
带有该属性的方法在执行第一个测试之前调用。 |
TestInitialize() |
带有该属性的方法在执行每个 TestMethod() 之前调用。 |
TestCleanup() |
带有该属性的方法在执行每个 TestMethod() 之后调用。 |
ClassCleanup() |
带有该属性的方法在执行 ALL 测试之后调用。 |
AssemblyCleanup() |
在执行为执行选择的第一个 TestClass() 中的第一个 TestMethod() 之后,执行带有该属性的方法。 |
Description() |
提供关于给定 TestMethod() 的描述。 |
Ignore() |
由于某种原因忽略 TestMethod() 或 TestClass()。 |
ExpectedException() |
当测试特定异常时,如果使用该属性指定的异常不是从实现代码引发,则测试不会失败。 |
我编写什么类型的测试?
一个方法及其相关测试之间很难有一对一关系。编写自动化单元测试需要开发人员“进行全面思考”,并了解关于对象的所有内容 — 它将如何消耗、使用、处理,以及在任何情况下如何起到积极、消极、非决定性作用。
例如,请考虑一个用于针对数据库中 Customer 项执行 CRUD(创建、检索、更新、删除)功能的典型对象方法。对于该对象的 Load() 方法,要针对以下方案编写测试:
• |
构造函数测试 — 确保对象正确加载,带有正确的信息。 |
• |
PositiveLoadScalarTest — 测试数据库中一个 Customer 的成功加载。 |
• |
NegativeLoadScalarTest — 测试一个 Customer 的失败加载,即该 Customer 不在数据库中。 |
• |
PositiveLoadTest — 基于已知数据测试 Customer 的成功加载。 |
• |
NegativeLoadTest — 测试数据库中不存在的 Customer 的失败加载。 |
• |
NegativeValidationTest — 确保验证逻辑正确工作。 |
这些只是自动化单元测试套件许多用法中的一部分。我曾经听说一个小团队使用单元测试查看针对其组件的已知安全攻击。从宏观的角度来看,单元测试应该明确保证组件的正常使用。具有丰富的测试集将使团队确信您已经准确实现了既定的目标:编写有效的软件。无论自信源自哪里 — 这就是您需要编写的测试。
您测试什么?
从本质上看,这些自动化单元测试非常低级。它们旨在测试下至构造函数、方法调用的对象,甚至是对象上的属性。
关于“公共对私有”的主题在单元测试派系中引发了许多争论。许多人认为单元测试只应该测试对象的公共接口。其他人认为应该测试每个调用 — 包括内部私有方法。VSTS 支持两个单元测试级别。VSTS 通过使用私有访问器或包装类支持私有测试,后者提供基于“私有”方法和属性生成单元测试的功能。
为什么生成代码?
阅读上面的列表后,您可能会想起前面项目的单个对象,并思考:“如果我用“这些”对象进行该操作,就需要编写大量代码!”请考虑开发人员仍编写“单元测试”代码的事实 — 只在不同的窗体(例如,前面提到的 WinForms 示例)上进行。此外,具有可自行记录、可重用的实现示例带来的好处远大于生成更多代码所带来的麻烦。最后,在单元测试中设计更多的环节已证明可以减少质量保证环节中的故障。
正如前面所提到的,代码生成是“软件创建软件”的过程。基于可重复的过程创建代码是理想的。例如,一些使用代码生成的较好示例包括:脚本数据、创建表示实体及其在储存库(数据库 CRUD)中存在的对象,或者创建适用于数据维护的 UI 控件。使用代码生成的好处包括:
• |
节省时间 — 为什么花几小时/天/周创建一些在几秒/分钟内就可以创建的内容? |
• |
强制标准/约定— 对于强制您的标准和命名约定而言,没有什么比消除开发中的人员因素并依赖基于“您的”规则的可重复过程更好的了。 |
• |
测试私有方法的功能 — 正如本文前面提到的,Unit Testing Framework 提供使用“私有访问器”类测试私有方法的功能。该代码生成引擎创建与这些访问器类相关的所有“基础代码”。 |
• |
获取现有组件的信息 — 搜索另一个开发人员的组件吗?基于这些组件生成代码可能提供关于该实现以及该对象接口的简明示例。此外,进行设计并在编码之前“清除”其对象的公共接口(例如,通过使用 VS 2005 类设计器)的开发人员将极大地受益于代码生成引擎。 |
正如您所预期的,自动化单元测试属于“优秀代码生成候选者”。在现有组件中指出一些内容并针对这些自动单元测试生成初始代码难道不是很理想吗?而且不只是保留单元测试框架,还会围绕对象的公共接口生成实现示例吗?将来的 Visual Studio 2005 Team System 用户将拥有该功能以及更多功能!
让我们生成一些代码吧!
本例中,我们将生成本文前面提到的 BankAccount 类的代码。本文的这一部分旨在为您介绍代码生成过程,并重点介绍所提供的功能以及从 VSTS 使用 Unit Testing 引擎的好处。
第 1 步:创建您的实现代码
首先,我们创建一个将用作应用程序的业务层的类库项目。
要在 VS 2005 中创建该库:
1. |
启动 Visual Studio 2005。 |
2. |
单击 File | New | Project。 |
3. |
选择您选定的语言 Windows,并选择 Class Library 项目模板。 |
4. |
将 Name 和 Solution Name 设置为 BankAccountDemo.Business,选择一个位置,并单击 OK 来创建该类库。 |
VS 2005 创建该类后,下一个任务就是创建针对您的项目设计的 BankAccount 类。为此,需要执行以下操作:
1. |
在解决方案资源管理器中单击右键,并单击 Delete,从项目中移除该文件并将其从硬盘中删除。 |
2. |
右键单击 BankAccountDemo.Business 项目,然后单击 Add,之后单击 Class。 |
3. |
选择文件名 BankAccount.cs,并单击 Add 创建类文件。 |
4. |
针对 BankAccount.cs 文件对代码进行以下更改。 |
第 2 步:生成您的初始单元测试代码
由于 Unit Testing 引擎内置于 Visual Studio 2005 Team System,因此生成代码比以前更容易。除了生成单元测试结构之外,它将生成特定于实例的信息,例如,对象创建、类型化参数和方法执行。
VS 2005 提供在任何类结构级别生成单元测试代码的功能,这些级别包括命名空间、类、方法、属性、构造函数,等等。可通过右键单击这些代码元素并单击 Generate test(s)(图 3)进行此操作。
图 3. Generate test(s) 方法
因此,要开始代码生成过程,请执行以下步骤:
• |
右键单击该类名 BankAccount 并单击 Create Tests。 |
现在,应该为您提供 Generate Unit Tests 对话框(如图 4 所示)。该对话框及其组件提供对该过程中生成的代码进行自定义的功能。让我们看一下所有这些元素。
图 4. 生成 Unit Tests 对话框
Current selection: 树视图允许导航自定义类及其元素。VS 2005 使用反射填充该树视图,并在右键单击以及单击 Create Tests 的位置自动选择组件。图 3 中,由于我在类级别进行了此操作,因此该对话框自动选择用于代码生成的所有类元素。如果选择在单个级别(即,构造函数、属性或方法)进行生成,则只选择那些元素。
Filter 选项(位于右上角)提供修改树视图(图 5)中所示结果的功能,包括显示非公共项、基类型以及“只属于我的代码”。如果使用的是大型解决方案,或者感觉显示私有的内部结构会弄乱选择窗口,那么这对您很有益处。
图 5. 筛选选择结果
下一个是 Output project 对话框(位于 Current selection: 树视图下)。该列表框允许您针对生成的测试装置选择目的项目(图 6)。如果您的解决方案包含以前创建的测试项目,则将包含该测试项目以供选择。由于这是我们首次访问该对话框,因此可以选择 Create a new … Test Project 选项。
图 6. 输出项目选择
要继续我们的过程,请执行以下操作:
• |
基于您选择的语言选择 Create a new {0} Test Project。 |
最后,该对话框提供通过 Settings 按钮(位于左下角)自定义代码生成过程的功能。单击该按钮将加载 Test Generation Settings 对话框,如图 7 所示。
图 7. Test Generation Settings 对话框
该对话框允许您进行以下更改:
• |
更改命名约定,它们用于生成针对文件、类(测试装置)和方法(测试)的名称。 |
• |
打开/关闭使所有测试结果在默认情况下为 Inconclusive 的功能。选择该选项将在每个生成的 Test() 方法中包含以下占位符语句。 Assert.Inconclusive("TODO: Implement code to verify target"); |
• |
打开/关闭启用生成警告的功能 — 即,如果在代码生成过程中出现任何警告,都要进行报告。 |
• |
在全局范围内限制所有类型。该设置通知代码生成引擎将一个全局限定符(在 Microsoft Visual C# 2005 中是 global::)添加到变量声明中。当在多个命名空间中具有名称相似的对象时,请使用该设置。否则,代码生成引擎将创建逻辑来创建该对象,但是编译器不能确定创建哪个类,因此会产生错误。 |
• |
启用/禁用针对已具有测试的项生成测试的功能。下面我们将讨论关于后续代码生成尝试的主题。 |
• |
启用/禁用文档注释。这允许您在使用每个 Test() 方法时禁用 XML 文档的创建 |
要完成我们的配置并生成单元测试代码(以及更多),请执行以下操作:
1. |
单击 OK 按钮开始代码生成过程。 |
2. |
输入名称 BankAccountDemo.Business.Test 作为新项目名,并单击 Create 按钮完成该过程。 |
VS 2005 将显示一个进度栏,提供代码生成过程中的状态。该过程将在几秒钟内完成,您可以看到一个名为 BankAccountTest.cs 的类。
生成了什么?
在我们对该测试装置进行特别查看之前,让我们看一下在代码生成过程中创建了什么。
首先,它创建了 Test Class Library 项目 BankAccountDemo.Business.Test。请注意该项目如何包含对实现类 BankAccountDemo.Business(您从其中生成代码)和 Microsoft.VisualStudio.QualityTools.UnitTestFramework 类库的引用。在查看该类的内容时,您将注意到以下文件:
• |
AuthoringTests.txt — 这是一些信息性的内容,定义如何使用单元测试(打开、查看、运行、查看结果、更改测试的运行方式),以及 VSTS 中包含的不同测试类型的定义。 |
• |
ManualTest1.mht — 这是 VSTS 中使用的手动测试套件,用于执行测试并报告结果。手动测试是 VSTS 支持的一个附加测试类型。有关更多信息,请参阅 MSDN 资源库的“手动测试”主题。 |
• |
UnitTest1.cs — 这是一个引用类,它只提供一个基单元测试(包括 TestClass、TestInitialize、TestCleanup 和 TestMethod 的定义)。 |
• |
BankAccountTest.cs — 这是特定于程序集生成的单元测试代码。让我们仔细看看该代码,它是代码生成过程中最重要的部分。 |
由 Unit Testing 引擎生成的类包括以下组件:
• |
Using/imports 语句,用于引用的程序集。 |
• |
TestClass() 定义,用于包含该测试的类 (BankAccountTestFixture)。 |
• |
一个私有访问器和用于 TestContext 的公共属性。它由单元测试运行器(即 VSTS Unit Test Framework)使用,以便提供关于当前测试运行的信息以及用于该运行的功能。 |
• |
TestInitialize() 和 TestCleanup() 方法。这些方法常用于获取和释放测试所需的任何对象。 |
• |
TestMethod(),用于每个选定的方法。 |
让我们仔细看一下 DepositMoneyTest(),它负责确保当前的平衡能反映原始数量与累计数量的总和。
请注意该生成引擎除创建一个 stub TestMethod() 对象外,是如何进行其他操作的。它创建了适用于接口的示例单元测试,包括:
• |
BankAccount 对象的分配和结构(测试的对象主题) |
||
• |
本地变量的创建和默认分配,这些变量表示作为该测试主题的方法/构造函数所需的参数。
|
||
• |
如果测试基于一个源对象方法调用,则生成的代码将包含对该方法(带有用于这些参数的局部变量)的调用。 |
||
• |
初始 Assert() 方法调用,基于该方法的返回值。 |
||
• |
Assert.Inconclusive() 方法调用,作为完成测试代码的提示程序。非确定性测试将在 Test Results 对话框中显示为失败。 |
生成后:我现在需要做什么?
考虑要完成相同的操作可以不必做哪些事情,则通常可以认识到代码生成的好处。在我们的示例中,我们不必:
• |
创建单元测试项目。 |
• |
设置项目引用。 |
• |
添加适当的测试类(一个或多个)。 |
• |
生成主干 Unit Test Framework 类和属性。 |
• |
创建单个测试方法。 |
• |
创建特定于接口的逻辑。 |
由于代码生成过程创建了特定于对象接口的示例单元测试,因此我们接近于初始测试的完成阶段了。通常情况下,只需“填充空白”并完成断言(一个或多个),方法是将“已知的数据值”分配给属性变量并创建适当的 Assert() 方法。显然,这不是针对所有测试的示例,特别是对具有多个断言的复杂测试而言。
只需几秒钟的时间(使用相对较少的击键),您就能够将生成的单元测试代码转换为这些实际的测试。
例如,请考虑我们以如下方式开始。
我们能够完成相对容易且具有有限击键的测试(更改部分用黑体表示)。
重新生成单元测试代码
好消息是,代码生成过程不会让您重写以前生成(和修改)的单元测试。使用 Visual Studio 2005 Team System 的 Beta 2 版本,代码生成选项提供一个启用/禁用创建已存在测试的复选框。如果选择它,而且该过程找到了一个具有相同名称的现有测试,则该过程将忽略该测试方法,并创建后续测试,从而将一个数字附加到该方法名的末尾。这通常在对象中使用重载的方法或构造函数时发生,或者当单击 Generate 按钮而不取消选定现有测试时发生。
自动化单元测试建议
虽然本节可以独立成文,但这里只是一些您在创建单元测试时可以采纳的基本建议。
• |
设计彼此独立的单元测试,其中它们可以独立运行(由于可以通过测试 UI 随意选择或取消选定它们)。 |
||||
• |
不要只进行正面测试。请确保代码能够响应任何方案,包括发生意外时(资源不可用,数据库只读等)。 |
||||
• |
把自己当作一个 QA 人员,想象成一个测试人员,而不仅仅是一个开发人员。您花在设计单元测试上的时间将有助于减少日后解决故障所用的时间。请注意对象的几个小细节:数据如何在对象之间传输?谁使用它们?销毁对象容易吗?如果我“进行此操作”,将会发生什么? |
||||
• |
跳出您自己的思维模式。尽可能多地对测试进行头脑风暴。当您完成时,回头查看您可能漏掉的内容。来自团队成员的请求反馈 — 例如,他们创建了什么其他类型的测试?其他人可能提供一个对熟悉自己代码的开发人员而言非常困难的观点。 |
||||
• |
代码覆盖。使用 VSTS 代码覆盖规范提供有关每个测试运行中实际执行多少代码的信息(代码的行数,占所有代码的百分比)。如果编码完成,并且通过了所有测试,但代码覆盖显示只执行了该逻辑的一小部分,那么您的测试真的成功了吗?高代码覆盖不一定意味着您具有一个完整的“测试”集,而未覆盖的代码通常非常适用于一个新的测试用例。 |
||||
• |
当生成单元测试时,要帮助其他人了解您的代码:
|
||||
• |
此外,当其他所有测试都失败时,请进行调试。自动化单元测试应该有助于减少您用在调试器上的时间。但是,如果测试结果和代码覆盖无法提供测试失败的原因,那么您大可不必担心调试单元测试。从 Beta 2 版的 Visual Studio 2005 Team System 开始,开发人员可以使用 Test Manager 中的 Debug checked tests 选项调试他们的单元测试程序集。 |
小结
自动化单元测试为开发环节提供了一个结构化、自行纪录、高度便携且可重复的过程。如果在搜索现有程序集,或者如果开发环境需要在开始开发之前进行完整的设计,则请考虑使用内置到 Microsoft Visual Studio 2005 Team System 中的代码生成引擎。Visual Studio 2005 Team System 的单元测试代码生成功能可以为您节省宝贵的时间,而且有助于强制团队的开发标准和约定。通过生成用于自动化单元测试的基本内容,包括生成带有对象创建的测试方法、参数变量和基断言类,您应该能够顺利地在您的开发方法论中采用自动化单元测试。
作者简介
作为针对 telligent 系统的专业服务主管,Scott Dockendorf 擅长于用 .NET 提供高性能、可伸缩的应用程序。Scott 热衷于解决方案体系结构、安全开发,他通过标准和已通过验证的方法论帮助企业采用推荐的实践。Scott 是 .NET 社区的一个积极成员,他自愿作为 North Dallas .NET User Group 的程序主管。他还参加了面向本地 .NET 用户组的会议,而且是 INETA's International Academic Committee for Texas 的活跃分子。您可以通过他的电子邮件 (scottd@telligent.com) 或网络日记 (http://weblogs.asp.net/scottdockendorf) 与他联系。