Live Unit Testing 入门

在 Visual Studio 解决方案中启用 Live Unit Testing 后,它可以直观描述测试覆盖率和测试状态。 Live Unit Testing 还会在你每次修改代码时动态执行测试,并在更改导致测试失败时立即发出通知。

Live Unit Testing 可用于测试面向 .NET Framework、.NET Core 或 .NET 5+ 的解决方案。 在本教程中,你将通过创建面向 .NET 的简单类库来了解 Live Unit Testing 的用法,并创建面向 .NET 的 MSTest 项目来对其进行测试。

可从 GitHub 上的 MicrosoftDocs/visualstudio-docs 存储库下载完整的 C# 解决方案。

先决条件

本教程需要你已安装具有 .NET 桌面开发工作负载的 Visual Studio Enterprise 版本

创建解决方案和类库项目

首先,创建名为 UtilityLibraries 的 Visual Studio 解决方案,其中包含单个 .NET 类库项目 StringLibrary。

解决方案只是一个可以存储一个或多个项目的容器。 要创建空白解决方案,请打开 Visual Studio 并执行以下操作:

  1. 从顶级的 Visual Studio 菜单中依次选择“文件”>“新建”>“项目” 。

  2. 在模板搜索框中键入“解决方案”,然后选择“空白解决方案”模板 。 将该项目命名为“UtilityLibraries” 。

  3. 完成解决方案的创建。

现在已创建解决方案,接下来需要创建一个名为 StringLibrary 的类库,其中包含大量用于处理字符串的扩展方法。

  1. 在“解决方案资源管理器”中,右键单击 UtilityLibraries 解决方案,然后选择“添加”>“新建项目” 。

  2. 在模板搜索框中键入“类库”,然后选择面向 .NET 或 .NET Standard 的“类库”模板。 单击“下一步”。

  3. 将该项目命名为“StringLibrary” 。

  4. 单击“创建”以创建项目 。

  5. 将代码编辑器中的所有现有代码替换为以下代码:

    using System;
    
    namespace UtilityLibraries
    {
        public static class StringLibrary
        {
            public static bool StartsWithUpper(this string s)
            {
                if (String.IsNullOrWhiteSpace(s))
                    return false;
    
                return Char.IsUpper(s[0]);
            }
    
            public static bool StartsWithLower(this string s)
            {
                if (String.IsNullOrWhiteSpace(s))
                    return false;
    
                return Char.IsLower(s[0]);
            }
    
            public static bool HasEmbeddedSpaces(this string s)
            {
                foreach (var ch in s.Trim())
                {
                    if (ch == ' ')
                        return true;
                }
                return false;
            }
        }
    }
    

    StringLibrary 有三种静态方法:

    • 如果字符串以大写字符开头,则 StartsWithUpper 返回 true;否则返回 false

    • 如果字符串以小写字符开头,则 StartsWithLower 返回 true;否则返回 false

    • 如果字符串包含嵌入的空格字符,则 HasEmbeddedSpaces 返回 true;否则返回 false

  6. 从顶级 Visual Studio 菜单中依次选择“生成”>“生成解决方案” 。 生成应该成功。

创建测试项目

下一步是创建单元测试项目,以测试 StringLibrary 库。 通过执行以下步骤创建单元测试:

  1. 在“解决方案资源管理器”中,右键单击 UtilityLibraries 解决方案,然后选择“添加”>“新建项目” 。

  2. 在模板搜索框中键入“单元测试”,选择“C#”作为语言,然后选择 .NET 的“MSTest 单元测试项目”模板。 单击“下一步”。

    注意

    在 Visual Studio 2019 版本 16.9 中,MSTest 项目模板名称是“单元测试项目”。

  3. 将项目命名为“StringLibraryTests”,然后单击“下一步”。

  4. 选择建议的目标框架或 .NET 8,然后选择“创建”。

    注意

    此入门教程使用 Live Unit Testing 的 MSTest 测试框架。 还可使用 xUnit 和 NUnit 测试框架。

  5. 单元测试项目无法自动访问它正在测试的类库。 可以通过添加对类库项目的引用来提供测试库访问权限。 为此,请右键单击 StringLibraryTests 项目,然后选择“添加”>“项目引用”。 在“引用管理器”对话框中,确保“解决方案”选项卡处于选中状态,然后选择 StringLibrary 项目,如下图所示 。

    “引用管理器”对话框

    “引用管理器”对话框

  6. 将模板提供的样本单元测试代码替换为以下代码:

    using System;
    using Microsoft.VisualStudio.TestTools.UnitTesting;
    using UtilityLibraries;
    
    namespace StringLibraryTest
    {
        [TestClass]
        public class UnitTest1
        {
            [TestMethod]
            public void TestStartsWithUpper()
            {
                // Tests that we expect to return true.
                string[] words = { "Alphabet", "Zebra", "ABC", "Αθήνα", "Москва" };
                foreach (var word in words)
                {
                    bool result = word.StartsWithUpper();
                    Assert.IsTrue(result,
                                  $"Expected for '{word}': true; Actual: {result}");
                }
            }
    
            [TestMethod]
            public void TestDoesNotStartWithUpper()
            {
                // Tests that we expect to return false.
                string[] words = { "alphabet", "zebra", "abc", "αυτοκινητοβιομηχανία", "государство",
                                   "1234", ".", ";", " " };
                foreach (var word in words)
                {
                    bool result = word.StartsWithUpper();
                    Assert.IsFalse(result,
                                   $"Expected for '{word}': false; Actual: {result}");
                }
            }
    
            [TestMethod]
            public void DirectCallWithNullOrEmpty()
            {
                // Tests that we expect to return false.
                string[] words = { String.Empty, null };
                foreach (var word in words)
                {
                    bool result = StringLibrary.StartsWithUpper(word);
                    Assert.IsFalse(result,
                                   $"Expected for '{(word == null ? "<null>" : word)}': " +
                                   $"false; Actual: {result}");
                }
            }
        }
    }
    
  7. 通过选择工具栏上的“保存”图标来保存你的项目 。

    由于单元测试代码包含一些非 ASCII 字符,因此将显示以下对话框来进行警告,如果以其默认的 ASCII 格式保存文件,某些字符将会丢失。

  8. 选择“以其他编码保存”按钮 。

    选择文件编码

    选择文件编码

  9. 在“高级保存选项”对话框的“编码”下拉列表中,选择“Unicode (UTF-8 无签名) - 代码页 65001”,如下图所示 :

    选择 UTF-8 编码

  10. 从顶级 Visual Studio 菜单中选择“生成”>“重新生成解决方案”,编译单元测试项目 。

已创建一个类库并为其编写了几个单元测试。 现在已完成使用 Live Unit Testing 所需的预备工作。

启用 Live Unit Testing

到目前为止,尽管已为 StringLibrary 类库编写测试,但尚未执行。 启用 Live Unit Testing 后,就会自动执行这些测试。 为此,请执行以下操作:

  1. 选择包含 StringLibrary 代码的代码编辑器窗口(可选)。 可以是 C# 项目的 Class1.cs,或者 Visual Basic 项目的 Class1.vb 。 (启用 Live Unit Testing 后,通过此步骤可直观检查测试结果和代码覆盖率的范围。)

  2. 从顶级 Visual Studio 菜单中依次选择“测试”>“Live Unit Testing”>“启动” 。

  3. 通过确保存储库根路径包含实用工具项目和测试项目的源文件的路径来验证 Live Unit Testing 的配置。 选择“下一步”,然后选择“完成”。

  1. 在“Live Unit Testing”窗口中,选择“包括所有测试”链接(或者,选择“播放列表”按钮图标,然后选择“StringLibraryTest”,这会选择其下的所有测试。然后取消选择“播放列表”按钮以退出编辑模式。)

  2. Visual Studio 将重新生成项目并启动 Live Unit Testing,使其自动运行所有测试。

  1. Visual Studio 将重新生成项目并启动 Live Unit Testing,使其自动运行所有测试。

运行完测试后,Live Unit Testing 同时显示总体结果和各个测试的结果 。 此外,代码编辑器窗口以图形方式显示测试代码覆盖率和测试结果。 如下图所示,三项测试均已成功执行。 它还显示测试中已覆盖 StartsWithUpper 方法中的所有代码路径,并已成功执行这些测试(用绿色复选标记“✓”指示)。 最后,显示 StringLibrary 中的其他方法都没有代码覆盖率(用蓝线“➖”指示)。

启动 Live Unit testing 后的实时测试资源管理器和代码编辑器窗口

启动 Live Unit testing 后的实时测试资源管理器和代码编辑器窗口

还可通过在代码编辑器窗口中选择一个特定的代码覆盖率图标来获得有关测试覆盖率和测试结果的更多详细信息。 若要查看此详细信息,请执行以下操作:

  1. 单击 StartsWithUpper 方法中写着 if (String.IsNullOrWhiteSpace(s)) 的行上的绿色复选标记。 如下图所示,Live Unit Testing 指示三个测试均覆盖该代码行,并且都已成功执行。

    if 条件语句的代码覆盖率

    if 条件语句的代码覆盖率

  2. 单击 StartsWithUpper 方法中写着 return Char.IsUpper(s[0]) 的行上的绿色复选标记。 如下图所示,Live Unit Testing 指示只有两个测试均覆盖该代码行,并且都已成功执行。

    return 语句的代码覆盖率

    return 语句的代码覆盖率

Live Unit Testing 标识的主要问题是代码覆盖率不完整。 此问题将在下一部分得以解决。

展开测试覆盖率

此部分将把单元测试扩展到 StartsWithLower 方法。 执行此操作时,Live Unit Testing 将以动态方式继续测试代码。

若要将代码覆盖率扩展到 StartsWithLower 方法,请执行以下操作:

  1. 将以下 TestStartsWithLowerTestDoesNotStartWithLower 添加到项目的测试源代码文件中:

    // Code to add to UnitTest1.cs
    [TestMethod]
    public void TestStartsWithLower()
    {
        // Tests that we expect to return true.
        string[] words = { "alphabet", "zebra", "abc", "αυτοκινητοβιομηχανία", "государство" };
        foreach (var word in words)
        {
            bool result = word.StartsWithLower();
            Assert.IsTrue(result,
                          $"Expected for '{word}': true; Actual: {result}");
        }
    }
    
    [TestMethod]
    public void TestDoesNotStartWithLower()
    {
        // Tests that we expect to return false.
        string[] words = { "Alphabet", "Zebra", "ABC", "Αθήνα", "Москва",
                           "1234", ".", ";", " "};
        foreach (var word in words)
        {
            bool result = word.StartsWithLower();
            Assert.IsFalse(result,
                           $"Expected for '{word}': false; Actual: {result}");
        }
    }
    
  2. 在调用 Microsoft.VisualStudio.TestTools.UnitTesting.Assert.IsFalse 方法之后,立即添加以下代码来修改 DirectCallWithNullOrEmpty 方法。

    // Code to add to UnitTest1.cs
    result = StringLibrary.StartsWithLower(word);
    Assert.IsFalse(result,
                   $"Expected for '{(word == null ? "<null>" : word)}': " +
                   $"false; Actual: {result}");
    
  3. 在你修改源代码时,Live Unit Testing 将自动执行新增的和修改后的测试。 如下图所示,所有测试(包括已添加的两个测试和已修改的测试)都已成功。

    展开测试覆盖率之后的实时测试资源管理器

    展开测试覆盖率之后的实时测试资源管理器

  4. 切换到包含 StringLibrary 类源代码的窗口。 Live Unit Testing 现在显示代码覆盖率已扩展到 StartsWithLower 方法。

    StartsWithLower 方法的代码覆盖率

    StartsWithLower 方法的代码覆盖率

在某些情况下,“测试资源管理器”中的成功测试可能会灰显。指示某个测试当前正在执行,或测试没有再次运行,因为测试自上次执行之后不会受到任何代码更改带来的影响。

到目前为止,所有测试都已成功。 在接下来的部分中,我们将讨论如何处理测试失败的问题。

处理测试失败

此部分将介绍如何使用 Live Unit Testing 标识、故障排除并解决测试失败的问题。 需要将测试覆盖率扩展到 HasEmbeddedSpaces 方法来执行此操作。

  1. 将以下方法添加到测试文件:

    [TestMethod]
    public void TestHasEmbeddedSpaces()
    {
        // Tests that we expect to return true.
        string[] phrases = { "one car", "Name\u0009Description",
                             "Line1\nLine2", "Line3\u000ALine4",
                             "Line5\u000BLine6", "Line7\u000CLine8",
                             "Line0009\u000DLine10", "word1\u00A0word2" };
        foreach (var phrase in phrases)
        {
            bool result = phrase.HasEmbeddedSpaces();
            Assert.IsTrue(result,
                          $"Expected for '{phrase}': true; Actual: {result}");
        }
    }
    
  2. 测试执行时,Live Unit Testing 指示 TestHasEmbeddedSpaces 方法失败,如下图所示:

    报告失败测试的实时测试资源管理器

    报告失败测试的实时测试资源管理器

  3. 选择显示库代码的窗口。 Live Unit Testing 已将代码覆盖率扩展到 HasEmbeddedSpaces 方法。 它还报告测试失败,方法是将一个红色“”添加到被失败的测试覆盖的行。

  4. 将鼠标悬停在有 HasEmbeddedSpaces 方法签名的行上。 Live Unit Testing 会显示一个工具提示,报告该方法被某个测试覆盖,如下图所示:

    关于失败的测试的 Live Unit Testing 信息

    关于失败的测试的 Live Unit Testing 信息

  5. 选择失败的“TestHasEmbeddedSpaces”测试 。 Live Unit Testing 提供了几个选项,如运行所有测试和调试所有测试,如下图所示:

    失败的测试的 Live Unit Testing 选项

    失败的测试的 Live Unit Testing 选项

  6. 选择“全部调试”,调试失败的测试 。

  7. Visual Studio 在调试模式下执行测试。

    测试将数组中的每个字符串分配给名为 phrase 的变量,并将其传递给 HasEmbeddedSpaces 方法。 程序执行暂停,并在断言表达式第一次为 false 时调用调试程序。 下图显示了 Microsoft.VisualStudio.TestTools.UnitTesting.Assert.IsTrue 方法调用中的意外值导致的异常对话框。

    Live Unit Testing 异常对话框

    Live Unit Testing 异常对话框

    此外,Visual Studio 提供的所有调试工具均可帮助我们对失败的测试进行故障排除,如下图所示:

    Visual Studio 调试工具

    Visual Studio 调试工具

    请注意,在“自动”窗口中,phrase 变量的值是“Name\tDescription”,它是数组的第二个元素 。 测试方法需要 HasEmbeddedSpaces 在传递该字符串时返回 true;而它返回 false。 显然,这是因为它无法将制表符“\t”识别为嵌入的空格。

  8. 请依次选择“调试”>“继续”,按 F5 或单击单击工具栏上的“继续”按钮,继续执行该测试程序 。 由于出现未经处理的异常,测试终止。 本文提供对 bug 进行初步调查的足够信息。 TestHasEmbeddedSpaces(测试例程)进行了错误的假设,或者 HasEmbeddedSpaces 无法正确识别所有嵌入的空格。

  9. 若要诊断并更正问题,请从 StringLibrary.HasEmbeddedSpaces 方法开始。 查看 HasEmbeddedSpaces 方法中的比较。 它认为嵌入的空格是U + 0020。 但是,Unicode 标准包含许多其他空格字符。 这表明库代码对空格字符进行了错误的测试。

  10. 将相等比较替换为对 System.Char.IsWhiteSpace 方法的调用:

    if (Char.IsWhiteSpace(ch))
    
  11. Live Unit Testing 会自动重新运行失败的测试方法。

    Live Unit Testing 将显示更新后的结果,这也会显示在代码编辑器窗口中。