演练:为托管代码创建和运行单元测试

本文逐步讲解如何使用托管代码的Microsoft单元测试框架和 Visual Studio 测试资源管理器创建、运行和自定义一系列单元测试。 从正在开发的 C# 项目开始,创建执行其代码的测试、运行测试并检查结果。 然后更改项目代码并重新运行测试。 若要在完成这些步骤之前对这些任务进行概念性概述,请参阅 单元测试基础知识。 如果要从现有代码自动生成测试,请参阅 从代码创建单元测试方法存根。

创建要测试的项目

  1. 打开 Visual Studio。

  2. 在开始窗口中,选择 创建新项目

  3. 搜索并选择 .NET 的 C# 控制台应用 项目模板,然后单击“下一步”

    备注

    如果未看到 控制台应用 模板,可以从 创建新项目 窗口安装它。 在“找不到所需内容?”消息中,选择“安装更多工具和功能”链接。 然后,在 Visual Studio 安装程序中,选择 .NET 桌面开发 工作负荷。

  4. 将项目命名为Bank,然后单击“下一步”

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

    银行项目已创建并显示在 解决方案资源管理器 中,Program.cs 文件在代码编辑器中打开。

    备注

    如果未在编辑器中打开 Program.cs,请双击 解决方案资源管理器中的文件 Program.cs 将其打开。

  5. Program.cs 的内容替换为定义类的以下 C# 代码,BankAccount

    using System;
    
    namespace BankAccountNS
    {
        /// <summary>
        /// Bank account demo class.
        /// </summary>
        public class BankAccount
        {
            private readonly string m_customerName;
            private double m_balance;
    
            private BankAccount() { }
    
            public BankAccount(string customerName, double balance)
            {
                m_customerName = customerName;
                m_balance = balance;
            }
    
            public string CustomerName
            {
                get { return m_customerName; }
            }
    
            public double Balance
            {
                get { return m_balance; }
            }
    
            public void Debit(double amount)
            {
                if (amount > m_balance)
                {
                    throw new ArgumentOutOfRangeException("amount");
                }
    
                if (amount < 0)
                {
                    throw new ArgumentOutOfRangeException("amount");
                }
    
                m_balance += amount; // intentionally incorrect code
            }
    
            public void Credit(double amount)
            {
                if (amount < 0)
                {
                    throw new ArgumentOutOfRangeException("amount");
                }
    
                m_balance += amount;
            }
    
            public static void Main()
            {
                BankAccount ba = new BankAccount("Mr. Bryan Walton", 11.99);
    
                ba.Credit(5.77);
                ba.Debit(11.22);
                Console.WriteLine("Current balance is ${0}", ba.Balance);
            }
        }
    }
    
  6. 解决方案资源管理器中右键单击文件并选择 重命名,将文件重命名为 BankAccount.cs

  7. 生成 菜单上,单击 生成解决方案(或 按 Ctrl + SHIFT + B)。

现在你有一个项目,其中包含可以测试的方法。 在本文中,测试侧重于 Debit 方法。 从帐户提取资金时,将调用 Debit 方法。

创建单元测试项目

  1. 在“文件”菜单上,选择“添加>新建项目

    提示

    也可以右键单击“解决方案资源管理器”中的解决方案,然后依次选择“添加”>“新建项目”

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

    备注

    在 Visual Studio 2019 版本 16.9 中,MSTest 项目模板 单元测试项目

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

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

    从 Visual Studio 2022 版本 17.10 开始,还可以选择测试运行程序。 对于测试运行程序,可以选择 VSTest 或 MSTest。 有关测试运行程序之间差异的详细信息,请参阅 Microsoft.Testing.Platform 和 VSTest 比较

    BankTests 项目已添加到 Bank 解决方案中。

  5. BankTests 项目中,添加对 Bank 项目的引用。

    解决方案资源管理器中,选择 BankTests 项目下的 依赖项,然后从右键单击菜单中选择 添加引用(或 添加项目引用)。

  6. 引用管理器 对话框中,展开 项目,选择 解决方案,然后选中 Bank 项。

  7. 选择“确定”

创建测试类

创建测试类以验证 BankAccount 类。 可以使用项目模板生成的 UnitTest1.cs 文件,但为该文件和类提供更具描述性的名称。

重命名文件和类

  1. 若要重命名该文件,请在 解决方案资源管理器中选择 BankTests 项目中的 UnitTest1.cs 文件。 在右键单击菜单中,选择 重命名(或按 F2),然后将文件重命名为 BankAccountTests.cs

  2. 若要重命名类,请将光标放在代码编辑器中的 UnitTest1 上,右键单击,然后选择 重命名(或按 F2)。 键入 BankAccountTests,然后按 Enter

BankAccountTests.cs 文件现在包含以下代码:

// The 'using' statement for Test Tools is in GlobalUsings.cs
// using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace BankTests
{
    [TestClass]
    public class BankAccountTests
    {
        [TestMethod]
        public void TestMethod1()
        {
        }
    }
}

添加 using 语句

using 语句 添加到测试类,以便能够在没有使用完全限定名称的情况下调用正在测试的项目。 在类文件的顶部,添加:

using BankAccountNS;

测试类要求

测试类的最低要求包括:

  • 在包含要在测试资源管理器中运行的单元测试方法的任何类上都需要 [TestClass] 属性。

  • 希望测试资源管理器识别的每个测试方法都必须具有 [TestMethod] 属性。

可以在没有 [TestClass] 属性的单元测试项目中具有其他类,并且可以在没有 [TestMethod] 属性的测试类中具有其他方法。 可以从测试方法调用这些其他类和方法。

创建第一个测试方法

在此过程中,你将编写单元测试方法来验证 DebitBankAccount 方法的行为。

至少需要检查三种行为:

提示

可以删除默认 TestMethod1 方法,因为本演练中不会使用它。

创建测试方法

第一个测试验证的是有效金额(即小于账户余额且大于零)是否能正确地从账户中提取。 将以下方法添加到该 BankAccountTests 类:

[TestMethod]
public void Debit_WithValidAmount_UpdatesBalance()
{
    // Arrange
    double beginningBalance = 11.99;
    double debitAmount = 4.55;
    double expected = 7.44;
    BankAccount account = new BankAccount("Mr. Bryan Walton", beginningBalance);

    // Act
    account.Debit(debitAmount);

    // Assert
    double actual = account.Balance;
    Assert.AreEqual(expected, actual, 0.001, "Account not debited correctly");
}

该方法非常简单:它使用开始余额设置新的 BankAccount 对象,然后提取有效金额。 它使用 Assert.AreEqual 方法检查结束余额是否符合预期。 单元测试中经常使用 Assert.AreEqualAssert.IsTrue等方法。 有关编写单元测试的更多概念性信息,请参阅 编写测试

测试方法要求

测试方法必须满足以下要求:

  • 使用 [TestMethod] 特性进行修饰。

  • 它将返回 void

  • 它不能有参数。

生成并运行测试

  1. 生成 菜单上,选择 生成解决方案(或 按 Ctrl + SHIFT + B)。

  2. 如果 测试资源管理器 未打开,请通过从顶部菜单栏(或 测试>Windows>)选择 >测试资源管理器(或 按 Ctrl + ET)来打开它。

  3. 选择 运行所有 来运行测试(或按 Ctrl + RV)。

    在测试运行时,测试资源管理器 窗口顶部的状态栏将进行动画处理。 在测试运行结束时,如果所有测试方法都通过,则条形图变为绿色,如果任何测试失败,则为红色。

    在这种情况下,测试失败。

  4. 选择 测试资源管理器 中的方法以查看窗口底部的详细信息。

修复代码并重新运行测试

测试结果包含描述失败的消息。 可能需要向下钻取才能查看此消息。 对于 AreEqual 方法,该消息显示预期内容和实际接收的内容。 你预计余额会减少,但余额反而因取款金额而增加。

单元测试已发现一个 bug:取款金额本应从帐户余额中减去,结果却添加到帐户余额中。

修复错误

若要更正错误,请替换 BankAccount.cs 文件中的行:

m_balance += amount;

替换为:

m_balance -= amount;

重新运行测试

测试资源管理器中,选择 “运行所有”以重新运行测试(或 按 Ctrl + RV)。 红色/绿色条变为绿色,指示测试已通过。

Visual Studio 2019 中的 Visual Studio 2019 中显示已通过的测试的文本资源管理器

Visual Studio 2019 中的 Visual Studio 2019 中显示已通过的测试的文本资源管理器

使用单元测试改进代码

本部分介绍分析、单元测试开发和重构的迭代过程如何帮助你使生产代码更加可靠且有效。

分析问题

你已创建了一个测试方法,用于确认 Debit 方法中正确扣除了有效金额。 现在,验证若借方金额为以下情况,该方法是否会引发 ArgumentOutOfRangeException

  • 大于余额,或
  • 小于零。

创建并运行新的测试方法

创建一个测试方法,验证借记金额小于零时的正确行为:

[TestMethod]
public void Debit_WhenAmountIsLessThanZero_ShouldThrowArgumentOutOfRange()
{
    // Arrange
    double beginningBalance = 11.99;
    double debitAmount = -100.00;
    BankAccount account = new BankAccount("Mr. Bryan Walton", beginningBalance);

    // Act and assert
    Assert.ThrowsException<System.ArgumentOutOfRangeException>(() => account.Debit(debitAmount));
}

使用 <xref:Microsoft.VisualStudio.TestTools.UnitTesting.Assert.ThrowsException%2A> 方法断言已引发正确的异常。 除非抛出 ArgumentOutOfRangeException,否则此方法会导致测试失败。 如果在借方金额小于零时,临时修改测试方法以引发更通用的 ApplicationException,则测试将正确运行,即测试将失败。

若要测试提取金额大于余额的情况,请执行以下步骤:

  1. 创建名为 Debit_WhenAmountIsMoreThanBalance_ShouldThrowArgumentOutOfRange的新测试方法。

  2. 将方法正文从 Debit_WhenAmountIsLessThanZero_ShouldThrowArgumentOutOfRange 复制到新方法。

  3. debitAmount 设置为大于余额的数字。

运行两个测试并验证它们是否通过。

继续分析

可以进一步改进要测试的方法。 使用当前实现时,我们无法知道哪个条件(amount > m_balanceamount < 0)导致在测试期间引发异常。 我们只知道在方法中引发了一个 ArgumentOutOfRangeException。 如果我们能判断出 BankAccount.Debit 中的哪个条件(amount > m_balanceamount < 0)导致抛出异常,那么我们可以确信我们的方法能够正常检查其参数。

再次查看正在测试的方法(BankAccount.Debit),并注意到这两个条件语句都使用 ArgumentOutOfRangeException 构造函数,该构造函数只使用参数的名称作为参数:

throw new ArgumentOutOfRangeException("amount");

可以使用构造函数来报告更丰富的信息:ArgumentOutOfRangeException(String, Object, String) 包括参数的名称、参数值和用户定义的消息。 可以重构受测方法以使用此构造函数。 更理想的做法是使用公开的类型成员来指定错误。

重构受测试的代码

首先,为类范围中的错误消息定义两个常量。 将定义置于测试中的类中,BankAccount

public const string DebitAmountExceedsBalanceMessage = "Debit amount exceeds balance";
public const string DebitAmountLessThanZeroMessage = "Debit amount is less than zero";

然后,修改 Debit 方法中的两个条件语句:

if (amount > m_balance)
{
    throw new System.ArgumentOutOfRangeException("amount", amount, DebitAmountExceedsBalanceMessage);
}

if (amount < 0)
{
    throw new System.ArgumentOutOfRangeException("amount", amount, DebitAmountLessThanZeroMessage);
}

重构测试方法

通过删除对 <xref:Microsoft.VisualStudio.TestTools.UnitTesting.Assert.ThrowsException%2A?displayProperty=nameWithType>的调用来重构测试方法。 将对 Debit() 的调用包装到 try/catch 块中、捕获预期的特定异常并验证其关联的消息。 Microsoft.VisualStudio.TestTools.UnitTesting.StringAssert.Contains 方法提供比较两个字符串的功能。

现在,Debit_WhenAmountIsMoreThanBalance_ShouldThrowArgumentOutOfRange 可能如下所示:

[TestMethod]
public void Debit_WhenAmountIsMoreThanBalance_ShouldThrowArgumentOutOfRange()
{
    // Arrange
    double beginningBalance = 11.99;
    double debitAmount = 20.0;
    BankAccount account = new BankAccount("Mr. Bryan Walton", beginningBalance);

    // Act
    try
    {
        account.Debit(debitAmount);
    }
    catch (System.ArgumentOutOfRangeException e)
    {
        // Assert
        StringAssert.Contains(e.Message, BankAccount.DebitAmountExceedsBalanceMessage);
    }
}

重新测试、重写和重新分析

目前,测试方法不会处理它应处理的所有情况。 如果所测试的方法 DebitdebitAmount 大于余额(或小于零)时未能引发 ArgumentOutOfRangeException,则该测试方法通过。 此方案不太好,因为您希望在未引发异常时测试方法会失败。

此结果是测试方法中的 bug。 若要解决此问题,请在测试方法末尾添加 Assert.Fail 断言来处理未引发异常的情况。

重新运行测试表明,如果捕获到正确的异常,测试现将失败catch 块捕获到该异常,但该方法继续执行,并在新的 Assert.Fail 断言处失败。 若要解决此问题,请在 return 块中的 StringAssert 后面添加 catch 语句。 重新运行测试会确认你已修复此问题。 Debit_WhenAmountIsMoreThanBalance_ShouldThrowArgumentOutOfRange 的最终版本如下所示:

[TestMethod]
public void Debit_WhenAmountIsMoreThanBalance_ShouldThrowArgumentOutOfRange()
{
    // Arrange
    double beginningBalance = 11.99;
    double debitAmount = 20.0;
    BankAccount account = new BankAccount("Mr. Bryan Walton", beginningBalance);

    // Act
    try
    {
        account.Debit(debitAmount);
    }
    catch (System.ArgumentOutOfRangeException e)
    {
        // Assert
        StringAssert.Contains(e.Message, BankAccount.DebitAmountExceedsBalanceMessage);
        return;
    }

    Assert.Fail("The expected exception was not thrown.");
}

结论

对测试代码的改进导致更可靠且信息丰富的测试方法。 但更重要的是,它们还改进了测试中的代码。

提示

本演练使用托管代码的Microsoft单元测试框架。 “测试资源管理器”还可以在具有“测试资源管理器”适配器的第三方单元测试框架中运行测试。 有关详细信息,请参阅 安装第三方单元测试框架

有关如何从命令行运行测试的信息,请参阅 VSTest.Console.exe 命令行选项