开始使用自动化白盒测试(和 Pex)





5.00/5 (18投票s)
Pex 是一个新工具,有助于理解 .NET 代码的行为,调试问题,并完全自动地创建涵盖所有极端情况的测试套件。
引言
Pex 是一个新工具,有助于理解 .NET 代码的行为,调试问题,并完全自动地创建涵盖许多极端情况的测试套件。在内部,Pex 的运行方式非常像专业的白盒测试人员:它运行代码,通过监控来了解其行为,最后通过深入思考程序行为,找出新的输入以覆盖之前未覆盖的分支。
只需单击一次即可从编辑器中启动 Pex,您准备好在自己的代码上运行它了吗?
本文分步介绍在 Visual Studio 2008 或 2010 CTP 中使用 Pex。具体来说,
- 如何在代码编辑器中通过几次点击分析现有代码,
- 如何创建重现 Pex 发现的问题的测试用例,
- 如何调试此类问题,
- 如何让 Pex 生成并保存整个测试套件,该套件小巧但通常能实现高代码覆盖率,
- 如何编写参数化单元测试,
- 为什么参数化单元测试将改变您编写单元测试的方式。
首次运行 Pex
如果您想在自己的计算机上跟随本教程,我们假设您已在计算机上安装了 Pex。
让我们从头开始,编写一些代码。我们将编写一个方法,将包含独立单词的 string
转换为首字母大写的标识符,其中原始单词的首字母将转换为大写,点号将转义为下划线。
我们首先创建一个新的 C# 类库来存放 StringExtensions
类,并对 Capitalize
实现进行初步尝试。
public static class StringExtensions
{
// convert 'hello world' to 'HelloWorld'
// punctuation is turned into '_', others are ignored.
public static string Capitalize(string value)
{
// WARNING: this sample is for demonstration only: it *contains* bugs.
var sb = new StringBuilder();
bool word = false;
foreach (var c in value)
{
if (char.IsLetter(c))
{
if (word)
sb.Append(c);
else
{
sb.Append(char.ToUpper(c));
word = true;
}
}
else
{
if (c == '!')
sb.Append('_');
word = false;
}
}
return sb.ToString();
}
}
右键单击方法中的代码,然后在上下文菜单中选择运行 Pex 探索。
运行 Pex 探索意味着什么? Pex 将使用不同的输入多次运行您的代码。请勿在可能启动真实火箭的代码上运行 Pex!
经过短暂的深入思考后,Pex 开始在单独的 Pex 探索结果窗口中以表格形式显示其分析结果。表格的每一行代表 Capitalize
的实际输入/输出行为。输入参数 value
的值显示在第二列,相应的返回值显示在结果列。如果引发异常,则显示在摘要/异常列中。
为什么 Pex 选择这些输入?它们是特殊的,还是随机选择的?乍一看,这些输入可能只是随机选择的字符序列。但仔细观察:输入仅包含字符 ':'、'p'、'J' 和 '\0'。这些字符分别代表标点符号、小写字母、大写字母以及其他所有字符。Pex 精确地选择了程序区分的这些等价类中的一个代表。您可能会看到略有不同的字符,具体取决于 Pex 底层使用的约束求解器的状态。请考虑以下问题:当有 2^16=65536 种不同的字符时,随机选择的输入包含标点符号的概率是多少?
当我们点击一行时,右侧会显示更多详细信息。此处,我们看到异常的堆栈跟踪。我们可以点击详细信息标题查看 Pex 为该行选择的输入的详细信息。
.NET 指南规定方法不应抛出 NullReferenceException
。我们可以自己修复此问题,或者可以简单地给某人发一封电子邮件或创建一个工作项,让其他人处理此问题(假设有其他人可以处理我们的问题)。发送到按钮就是为此目的而设的。
但让我们面对现实,我们必须自己修复代码。实际上,我们不必这样做——对于这类简单问题,Pex 可以为我们代劳。点击添加先决条件...,然后点击“应用”按钮。
Pex 会修改我们 Capitalize
方法的开头,并插入对 null
输入的检查,类似于以下代码片段:
public static string Capitalize(string value) {
if (value == null)
throw new ArgumentNullException("value");
...
}
当我们再次运行 Pex 时,我们不会看到失败的 NullReferenceException
,而是看到可接受的 ArgumentNullException
行为。红叉图标已变为友好的绿色复选标记(其上方有小太阳和磁盘图标,稍后将解释它们的含义)。
我们已经了解了 Pex 如何创建有趣输入和输出的表格,如何解释结果,以及如何修复 Pex 发现的问题。到目前为止,Pex 生成的所有结果都“存在于内存中”,现在让我们看看如何将它们保存在解决方案中。
保存测试并调试问题
Pex 在表格中列出的一些输入看起来很奇怪。如果我们可以针对此类输入调试代码并逐行进行单步调试,那将是很好的。实际上,使用 Pex 很容易!选择一行,然后点击 Capitalize “保存测试...” 按钮。
Pex 显示一个对话框,其中说明了 Pex 将要执行的一系列步骤:Pex 将创建一个新的测试项目,然后执行许多我们暂时忽略的小步骤,最后,Pex 将创建具有当前选定行测试输入的单元测试。
什么是单元测试?单元测试是一个不接受参数的方法,类似于应用程序的
Main
方法。当单元测试引发意外异常时,它会失败,否则它会通过。.NET 有许多框架(NUnit、MbUnit、VSTest 等)可以自动检测、执行和报告此类测试。在本教程中,我们使用 VSTest,它是 Visual Studio Professional 安装自带的。Pex 也可以与其他测试框架配合使用;请在CodePlex上查看是否已为您喜欢的测试框架编写了自定义扩展。
生成的项目包含几个文件。与设计器文件类似,与当前行对应的已保存测试的 C# 源代码存储在 StringExtensionsTest.Capitalize.g.cs 中(它隐藏在 StringExtensionsTest.cs 下方);与设计器文件不同的是,您确实不应该编辑隐藏的文件,因为 Pex 可能会稍后删除或重新生成其内容。
我们现在应该看到以下生成的已保存测试用例的 C# 代码。[TestMethod]
属性表示该方法是单元测试,而 [PexGeneratedBy(...)]
属性表示该测试是由 Pex 生成的。
[TestMethod]
[PexGeneratedBy(typeof(StringExtensionsTest))]
public void Capitalize03()
{
string s;
s = this.Capitalize("\0");
Assert.AreEqual<string>("", s);
}
返回 Capitalize
的源代码,并在第一行设置一个断点。
现在,我们可以点击 Pex 探索结果窗口中选定行详细信息下方的调试按钮。
现在我们可以逐行逐步执行代码。
为 VSTest 保存测试套件
此时,我们不仅可以保存单个测试,还可以将整个表格保存为测试套件。此测试套件可用作将来的回归测试套件,或作为快速运行的构建验证测试 (BVT) 套件,即每次将代码更改提交到源代码存储库时都可以执行的测试套件。
要保存整个表格,请在表格中选择一行,然后按 Ctrl-A。然后,点击选定行右侧的保存...按钮。
同样,Pex 将显示详细说明 Pex 保存测试为 C# 代码的各个步骤的对话框。按应用。
现在,我们拥有一个可以在没有 Pex 的情况下执行的完整测试套件。生成的测试由 VSTest(Visual Studio 内置的单元测试框架)自动识别。选择测试->窗口->测试视图。
选择一个测试,按 Ctrl-A 选择所有测试,然后在测试视图窗口中按运行选定项按钮来运行所有测试。
此时运行测试只会重现 Pex 之前报告的相同结果。但是,将来运行测试可能会发现程序行为的重大更改。
参数化单元测试的概览
我们还没有提到,但当我们保存测试时,Pex 不仅创建了(传统的)涵盖所有极端情况的单元测试,还为 Capitalize
方法创建了一个参数化单元测试 (PUT) 存根。
[TestClass]
[PexClass(typeof(StringExtensions))]
[PexAllowedExceptionFromTypeUnderTest(typeof(ArgumentException))]
public partial class StringExtensionsTest
{
[PexMethod]
public string Capitalize(string value)
{
// TODO: add assertions to method StringExtensionsTest.Capitalize(String)
string result = StringExtensions.Capitalize(value);
return result;
}
}
此方法位于 StringExtensionsTest.cs 文件中,而各个(传统的)单元测试则保存在 StringExtensionsTest.Capitalize.g.cs 中。...g.cs 文件绝不应手动修改。如果我们想自定义 Pex 生成测试的方式,或者想添加验证测试代码是否正常工作的检查,那么我们应该始终编辑顶级文件中的 PUT
。
例如,我们可以在此处修改 PUT
,以检查 Capitalize
是否未更改字母数量。
[PexMethod]
public void CapitalizeMaintainsLettersCount(string input)
{
string output = StringExtensions.Capitalize(input);
Assert.AreEqual(
LettersCount(input),
LettersCount(output));
}
static int LettersCount(string s)
{
return Enumerable.Count(s,
c => char.IsLetter(c) || c == '_');
}
另一个 PUT
可以检查 Capitalize
是否是幂等的,即,如果我们对一个已大写的 string
进行大写处理,我们会得到相同的 string
。
[PexMethod]
public void CapitalizeIsIdempotent(string input)
{
string capitalized = StringExtensions.Capitalize(input);
string capitalizedTwice = StringExtensions.Capitalize(capitalized);
Assert.AreEqual(capitalized, capitalizedTwice);
}
我们可以声明 Capitalize
的结果只包含字母和下划线。
[PexMethod]
public void CapitalizeReturnsOnlyLettersAndUnderscores(string input)
{
string output = StringExtensions.Capitalize(input);
PexAssert.TrueForAll(output,
c => char.IsLetter(c) || c == '_');
}
您明白了。
参数化单元测试与传统单元测试
参数化单元测试 (PUT) 的意义何在?好吧,它们允许您分离编写单元测试时经常混为一谈的两个关注点:
- 预期的程序行为的描述,
- 覆盖代码中极端情况所需的特定输入。
虽然您必须编写 PUT
,但像 Pex 这样的工具可以负责生成覆盖极端情况的测试输入。Pex 将这些输入保存为传统的单元测试。因此,您需要编写和维护的测试代码更少:当您更改实现中的极端情况时,您可以简单地使用 Pex 从参数化单元测试重新生成各个单元测试,并且您将恢复到高或完整的代码覆盖率,或者 Pex 可能会指出导致失败的极端情况。
您可以(也应该)在编写任何代码之前就开始编写 PUT。而且,当将值参数化没有意义时,您可以将 PUT 与传统单元测试混合使用。
延伸阅读
- 使用 Pex 进行代码挖掘 (教程),本文的扩展版本。
- 使用 Pex 进行参数化单元测试 (教程),关于参数化单元测试主题的详尽教程,其中包含 Pex 内部工作原理和许多配置选项的深入信息。
- 使用 Pex 进行有效测试的参数化测试模式,列出了大量测试模式以及编写参数化单元测试的其他有用技巧。
- 存根 - .NET 测试存根的简单框架,概述了如何测试参数为接口或抽象类的 C# 方法。
进一步观看
- PDC2008:研究:使用 Pex 进行契约检查和自动化测试生成。
- Channel9:Pex,.NET 的自动化探索性测试.
- Channel9:Visual Studio 2008 中的 Pex 入门
进一步提问
- MSDN 论坛,您所有的问题、反馈、bug 等。
历史
- 2009 年 1 月 28 日:添加了 Channel9 链接
- 2009 年 1 月 13 日:更新了链接
- 2008 年 11 月 21 日:初始版本