65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIconstarIcon

5.00/5 (18投票s)

2008年11月21日

Ms-PL

9分钟阅读

viewsIcon

130697

downloadIcon

619

Pex 是一个新工具,有助于理解 .NET 代码的行为,调试问题,并完全自动地创建涵盖所有极端情况的测试套件。

更新参见 Channel9 上的视频文章!

pex/newpexillustration.png

引言

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/DiggerRunPexExplorations.png

运行 Pex 探索意味着什么? Pex 将使用不同的输入多次运行您的代码。请勿在可能启动真实火箭的代码上运行 Pex!

经过短暂的深入思考后,Pex 开始在单独的 Pex 探索结果窗口中以表格形式显示其分析结果。表格的每一行代表 Capitalize 的实际输入/输出行为。输入参数 value 的值显示在第二列,相应的返回值显示在结果列。如果引发异常,则显示在摘要/异常列中。

pex/DiggerTestTable.png

为什么 Pex 选择这些输入?它们是特殊的,还是随机选择的?乍一看,这些输入可能只是随机选择的字符序列。但仔细观察:输入仅包含字符 ':''p''J''\0'。这些字符分别代表标点符号、小写字母、大写字母以及其他所有字符。Pex 精确地选择了程序区分的这些等价类中的一个代表。您可能会看到略有不同的字符,具体取决于 Pex 底层使用的约束求解器的状态。请考虑以下问题:当有 2^16=65536 种不同的字符时,随机选择的输入包含标点符号的概率是多少?

当我们点击一行时,右侧会显示更多详细信息。此处,我们看到异常的堆栈跟踪。我们可以点击详细信息标题查看 Pex 为该行选择的输入的详细信息。

pex/DiggerTestDetails.png

.NET 指南规定方法不应抛出 NullReferenceException。我们可以自己修复此问题,或者可以简单地给某人发一封电子邮件或创建一个工作项,让其他人处理此问题(假设有其他人可以处理我们的问题)。发送到按钮就是为此目的而设的。

pex/DiggerSendTo.png

但让我们面对现实,我们必须自己修复代码。实际上,我们不必这样做——对于这类简单问题,Pex 可以为我们代劳。点击添加先决条件...,然后点击“应用”按钮。

pex/diggeraddprecondition.png

Pex 会修改我们 Capitalize 方法的开头,并插入对 null 输入的检查,类似于以下代码片段:

public static string Capitalize(string value) {
    if (value == null)
        throw new ArgumentNullException("value");
    ...
}

当我们再次运行 Pex 时,我们不会看到失败的 NullReferenceException,而是看到可接受的 ArgumentNullException 行为。红叉图标已变为友好的绿色复选标记(其上方有小太阳和磁盘图标,稍后将解释它们的含义)。

pex/DiggerExpectedException.png

我们已经了解了 Pex 如何创建有趣输入和输出的表格,如何解释结果,以及如何修复 Pex 发现的问题。到目前为止,Pex 生成的所有结果都“存在于内存中”,现在让我们看看如何将它们保存在解决方案中。

保存测试并调试问题

Pex 在表格中列出的一些输入看起来很奇怪。如果我们可以针对此类输入调试代码并逐行进行单步调试,那将是很好的。实际上,使用 Pex 很容易!选择一行,然后点击 Capitalize “保存测试...” 按钮。

pex/DiggerSaveTest.png

Pex 显示一个对话框,其中说明了 Pex 将要执行的一系列步骤:Pex 将创建一个新的测试项目,然后执行许多我们暂时忽略的小步骤,最后,Pex 将创建具有当前选定行测试输入的单元测试。

pex/DiggerCodeUpdatePreview.png

什么是单元测试?单元测试是一个不接受参数的方法,类似于应用程序的 Main 方法。当单元测试引发意外异常时,它会失败,否则它会通过。.NET 有许多框架(NUnitMbUnitVSTest 等)可以自动检测、执行和报告此类测试。在本教程中,我们使用 VSTest,它是 Visual Studio Professional 安装自带的。Pex 也可以与其他测试框架配合使用;请在CodePlex上查看是否已为您喜欢的测试框架编写了自定义扩展。

生成的项目包含几个文件。与设计器文件类似,与当前行对应的已保存测试的 C# 源代码存储在 StringExtensionsTest.Capitalize.g.cs 中(它隐藏在 StringExtensionsTest.cs 下方);与设计器文件不同的是,您确实不应该编辑隐藏的文件,因为 Pex 可能会稍后删除或重新生成其内容。

pex/diggersolutionexplorer.png

我们现在应该看到以下生成的已保存测试用例的 C# 代码。[TestMethod] 属性表示该方法是单元测试,而 [PexGeneratedBy(...)] 属性表示该测试是由 Pex 生成的。

[TestMethod]
[PexGeneratedBy(typeof(StringExtensionsTest))]
public void Capitalize03()
{
    string s;
    s = this.Capitalize("\0");
    Assert.AreEqual<string>("", s);
}

返回 Capitalize 的源代码,并在第一行设置一个断点。

pex/diggerbreakpoint.png

现在,我们可以点击 Pex 探索结果窗口中选定行详细信息下方的调试按钮。

pex/diggerdebug.png

现在我们可以逐行逐步执行代码。

为 VSTest 保存测试套件

此时,我们不仅可以保存单个测试,还可以将整个表格保存为测试套件。此测试套件可用作将来的回归测试套件,或作为快速运行的构建验证测试 (BVT) 套件,即每次将代码更改提交到源代码存储库时都可以执行的测试套件。

要保存整个表格,请在表格中选择一行,然后按 Ctrl-A。然后,点击选定行右侧的保存...按钮。

pex/DiggerSaveAll.png

同样,Pex 将显示详细说明 Pex 保存测试为 C# 代码的各个步骤的对话框。按应用

现在,我们拥有一个可以在没有 Pex 的情况下执行的完整测试套件。生成的测试由 VSTest(Visual Studio 内置的单元测试框架)自动识别。选择测试->窗口->测试视图

pex/DiggerTestView.png

选择一个测试,按 Ctrl-A 选择所有测试,然后在测试视图窗口中按运行选定项按钮来运行所有测试。

pex/DiggerTestViewRunAll.png

此时运行测试只会重现 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 与传统单元测试混合使用。

延伸阅读

进一步观看

进一步提问

  • MSDN 论坛,您所有的问题、反馈、bug 等。

历史

  • 2009 年 1 月 28 日:添加了 Channel9 链接
  • 2009 年 1 月 13 日:更新了链接
  • 2008 年 11 月 21 日:初始版本
© . All rights reserved.