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) - Codepage 65001, 如下图所示:

    选择 UTF-8 编码

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

你已为其创建了一个类库以及一些单元测试。 现已完成使用 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 Test,该测试会自动运行所有测试。

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

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

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

启动 Live Unit TestingLive Test Explorer 和代码编辑器窗口

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

  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 类源代码的窗口。 实时单元测试现在显示我们的代码覆盖率已经扩展到 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 会显示一个工具提示,报告该方法被某个测试覆盖,如下图所示:

    有关失败的测试

    有关失败的测试

  5. 选择失败的“TestHasEmbeddedSpaces”测试。 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 显示更新的结果,这些结果也会显示在代码编辑器窗口中。