文章 SubZero:CaseBase,一个 C# 测试框架






4.08/5 (7投票s)
2005年1月27日
10分钟阅读

77398

1086
描述了一个用于 Visual Studio 和 C# 的测试框架。
引言
测试驱动开发(Test-Driven Development,TDD)在 Kent Beck 的同名著作中有最好的描述。我们在这里的目的是提供一个实际的练习,用我们用 C# 开发的测试框架来阐述 TDD 的亮点。市面上已经有一些出色的 TDD 和 C# 框架,最明显的是 NUnit、mbUnit 和 csUnit。我们在这里使用的测试框架与这些替代方案有几个不同之处。
首先,我们的测试框架是一个 Visual Studio 解决方案,可以像任何其他应用程序一样打开和运行。您通过添加要测试的应用程序项目和测试项目(测试所在之处)来使用该解决方案。然后,开发正常进行,您可以在运行的应用程序和 Visual Studio 之间来回切换。因此,这种方法比启动外部 EXE 来加载和管理您的测试感觉更自然。
另一个不同之处是,测试框架引入了 Master 和 Monitor 的概念。Master 是测试结果的持久表示,可以用来验证同一测试的未来结果。这与在测试本身中硬编码预期结果不同,当“正确”由系统中多个对象中的多个值表示时,这种技术就会失效。
Monitor 是一种用户界面,可帮助我们检查 Master 中的值。典型的测试序列是:运行测试,在 Monitor 中查看结果以确保测试正确运行,保存 Master,最后验证 Master 以确保其正确保存并且在反序列化后与实时测试结果匹配。
这些增强功能都没有谈及标准 TDD 技术的实用性。这些技术久经考验,已被证明是一种可靠、可行的技术。相反,我们是在 UI 测试的背景下开发这些技术的,UI 测试最初很适合传统的 TDD,但当测试涉及多个用户操作时,它就完全不同了。
构思测试
暂时可能有点只见树木不见森林。请放心,我们的目标是生产高质量的代码,而不是为了测试而测试。如果最初的练习看起来有点过头,请记住,在正常开发中,您每隔几千个测试才设置一次测试解决方案。
我们将开发一个简单的类:WidgetCollection
。WidgetCollection
将继承自 System.Collections.CollectionBase
,并强制列表中元素的唯一性。这个类必然是微不足道的,这样我们就可以专注于技术。
那么让我们考虑一些测试。首先,简单的测试。什么绝对应该有效?
- 添加后,集合中应该有一个对象。
- 添加然后移除,集合中应该没有对象。
- 添加两次相同的对象,应该抛出异常,说明第二个对象不是唯一的。
- 添加然后移除两次相同的对象,应该抛出异常,说明该对象不在集合中。
这些是基本测试,保证在正常使用下会发生。考虑一些边缘情况也很好。这些测试你可能认为不太可能发生,但至少是可能的。
- 添加一个空对象,应该抛出异常。
- 移除一个空对象,应该抛出异常。
一段时间以来,我们一直认为“没有糟糕的测试”。我认为我们真正想说的是,“任何测试都比没有测试好”。显然,能够测试最大量代码的最小测试集是最好的。我们建议不要过分担心这一点,而是回到“任何测试”的格言,因为 TDD 的目的是以对您有意义的方式稳步前进。把解决魔方的问题抛开;单纯的大量经验总会让你成为一个更好的测试人员。
准备解决方案
下一步是配置解决方案。我们首先复制存根出来的测试框架解决方案。我们称这个解决方案为 CaseBase。CaseBase 的组织方式如下:
主项目被定义为一个 Windows 应用程序。测试框架(一个用 C# 编写的程序集)支持测试的编写,并包含由主应用程序调用的 UI。MyTests 项目生成一个程序集,并且是您的测试的存储库。这个程序集通常根据它包含的测试来命名。MyLibrary 项目代表与测试一起开发的应用程序部分。它也生成一个程序集。我们可以在开发时向这个项目添加代码文件,或者完全删除这个项目并用我们应用程序中的一个替换它。如果我们保留这个项目,我们通常会给它一个应用程序特定的名称。最后,Testing.Core 项目包含可以在多个测试程序集之间共享的类型。这个核心是您放置那些不想在测试程序集中一遍又一遍地声明,但也不属于 MyLibrary(应用程序本身)的类型的地方。
让我们继续看看这是如何工作的。
首先,我们将 CaseBase 解压到某个目录。这里我们将使用“C:\CaseBase”。接下来,我们在 Visual Studio 中打开“CaseBase.sln”。我们将 myLibrary 重命名为“WidgetLibrary”(一个应用程序特定的名称),并将 myTests 重命名为“WidgetCollectionTests”。
注意:如果您有 Visual Studio 经验,您会知道重命名项目并不会重命名这些项目所在的底层操作系统文件夹。我们通常会在 Windows 资源管理器中重命名这些文件夹,然后手动编辑“.sln”以反映更改。您也可以在 Visual Studio 中排除项目,关闭 Visual Studio,在资源管理器中重命名文件夹,启动 Visual Studio,然后使用 Add | Existing Project... 菜单选项将项目重新添加回解决方案。出于我们的目的,让文件夹名称和项目名称不同步是可以的。然而,通常在生产环境中,您不希望这样做,因为它会给源文件带来相当大的混乱。
配置 CaseBase 后,我们应该能够运行解决方案。这样做会显示一些空表单。左侧的 Cases 表单显示了当前正在考虑的所有案例。右侧的 Monitor 显示了当前选定案例的结果。CaseBase 的设置就这些了。现在我们准备编写一些测试了。
编写测试
我们所有的测试都有一个共同点:它们都操作一个 WidgetCollection
对象。在 TDD 中,足够相似以至于依赖相同设置例程的测试通常被编码为 Fixture。我们使用 Fixture 模式来减少测试中的代码量。这里,我们通过将 WidgetCollectionTests 中存根案例类的名称从 myCase
更改为 WidgetCollectionCase
并按如下方式实现设置例程来实施 Fixture 模式
protected override void Setup()
{
_Widgets = new WidgetCollection();
TestObject = _Widgets;
}
TestObject
是在 Case
类中声明的一个特殊属性。此对象用于提取值并构建主控。
当案例完成后,我们希望 WidgetCollection
被垃圾回收。为了确保这一点,我们编写了一个拆卸例程。
protected override void TearDown()
{
_Widgets = null;
}
现在我们的测试可以从 WidgetCollectionCase
继承,并且只需要包含实际的测试代码。例如,第一个测试 AddWidget 将被编码为
public class Case0001AddWidget: WidgetCollectionCase
{
protected override void Test()
{
Widgets.Add(new Widget());
}
}
Widgets
是在 WidgetCollectionCase
中声明的受保护属性,允许所有派生案例访问正在测试的 WidgetCollection
对象。
此时,解决方案无法构建。我们还没有编写 WidgetCollection
或 Widget
。让我们接下来做这件事。
编写 Widgets
我们只编写足以通过第一个测试的 WidgetCollection
代码
using System.Collections;
public class WidgetCollection: CollectionBase
{
public void Add(Widget aWidget)
{
InnerList.Add(aWidget);
}
}
并且只编写足以获得干净编译的 Widget
类
public class Widget
{
}
接下来,我们构建并运行解决方案。
AddWidget 案例确实出现在 Cases 列表中。我们选择该案例来运行它,然后 Monitor 中显示以下内容:
请注意,没有显示任何值,只有表示创建了 WidgetCollection
对象的开始和结束条目。为了验证这些结果是否正确,我们需要了解更多信息。集合中部件的数量就足够了。所以让我们编写必要的类,以便在 Monitor 中显示这个值。
定义状态
案例的状态是一组代表测试结果的值。状态必须继承自 CaseState
,并且可以包含您定义为验证给定案例类型正确性相关的所有内容。CaseState
用于在 Monitor 中显示案例结果值,并将这些值保存为 Master。Case
和 CaseState
的模型如下:
为了将 WidgetCollectionCase
与案例状态关联起来,我们编写了
public class WidgetCollectionCase
{
public override Type StateType
{
get
{
return typeof(WidgetCollectionState);
}
}
}
WidgetCollectionState
将定义为
public class WidgetCollectionState: CaseState
{
public override void ExtractTestValues()
{
WidgetCollection widgets = (WidgetCollection) TestObject;
_Count = widgets.Count;
}
}
状态还必须是可序列化的。我们将使用标准的 .NET 框架序列化功能来处理这个问题。为了验证正确性,我们需要将状态与相同类型的另一个状态对象进行比较。我们还需要在 Monitor 中显示状态中的值,以便我们可以确保测试结果是正确的。这两个要求都由 MatchStick 基础设施处理。
编码 MatchStick
为了从 WidgetCollectionState
中提取计数,我们定义了两种类型:IWidgetCollectionCount
(它定义了一个只读整数属性)和 WidgetCollectionCountMatchStick
。一旦我们编码了这两种类型,我们按如下方式注册 MatchStick
MatchboxRegistry.Register(typeof(IWidgetCollectionCount),
typeof(WidgetCollectionCountMatchStick));
这里我们将接口与匹配棒关联起来。在给定测试完成后,接口将从状态对象中提取,匹配棒将被创建并赋予接口。然后,匹配棒将创建一个包含计数值的条目。如果匹配棒被赋予两个状态,它将创建一个表示两个计数相等的条目。然后此条目将显示在监视器中。不匹配的条目在监视器中显示为红色。匹配的条目显示为绿色。
测试验证过程图示如下
有了我们的 MatchStick,我们现在可以再次构建并运行应用程序。这次,当我们选择案例时,Monitor 显示了计数
完成案例
我们还有几个案例要编写,其中一些无法用简单的 Count
属性值来掌握。那些抛出异常的案例带来了两个问题。我们不能让它们穿透我们的应用程序顶部,并且我们需要验证抛出的异常是否是正确的异常。为了处理这些问题,我们可以这样编写异常案例:
public class Case0003AddSameObjectTwice
{
protected override void Test()
{
Widget widget = new Widget();
this.Widgets.Add(widget);
try
{
this.Widgets.Add(widget);
}
catch (NonUniqueValueException e)
{
this.State.ExceptionMessage = e.Message;
}
}
}
在这里,我们捕获预期的异常并将其消息保存到 WidgetCollectionState
的新属性中。为了在 Monitor 中显示该消息,我们还将编写一个接口和一个 MatchStick 来提取异常字符串。该案例的结果然后显示在 Monitor 中
检查每个案例的结果后,我们可以为所有案例生成主控并进行验证。我们通过右键单击“案例”列表并选择“生成所有主控”来完成此操作。然后框架运行每个案例,保存结果。每个案例旁边的图标变为黄色,表示主控存在但尚未验证。我们再次右键单击并选择“验证所有”。框架再次运行每个案例,将当前结果与主控中保存的结果进行比较。如果结果完全匹配,则案例图标设置为绿色。如果案例结果不匹配,则案例图标设置为红色,并且在监视器中以红色显示不同的值。
至此,我们所有的案例都是绿色的,我们已准备好继续进行后续开发。
在下一篇文章中,我们将使用 CaseBase 开始开发一个完全用 C# 编写的 UI 平台。
CaseBase 下载
- CaseBaseSource.zip - 28 KB。使用测试框架时的起点解决方案。
- TestingFrameworkSource.zip - 44 KB。测试框架的源代码。