演练:创建并运行托管代码的单元测试
本文将使用托管代码的 Microsoft 单元测试框架和 Visual Studio 测试资源管理器引导你逐步完成一系列单元测试的创建、运行和自定义 。 你将从正处于开发过程中的 C# 项目开始,创建执行该项目代码的测试,运行测试并检查结果。 然后,更改项目代码并重新运行测试。 若要在执行这些步骤之前了解这些任务的概念性概述,请参阅单元测试基础知识。 要从现有代码自动生成测试,请参阅从代码创建单元测试方法存根。
创建一个项目进行测试
打开 Visual Studio。
在“开始”窗口上,选择“创建新项目” 。
搜索并对 .NET 选择 C#“控制台应用”项目模板,然后单击“下一步”。
注意
如果未看到“控制台应用”模板,则可以通过“创建新项目”窗口安装该模板 。 在“找不到所需内容?”消息中,选择“安装更多工具和功能”链接 。 然后在 Visual Studio 安装程序中,选择“.NET 桌面开发”工作负载。
将项目命名为“Bank”,然后单击“下一步” 。
选择建议的目标框架或 .NET 8,然后选择“创建”。
将创建 Bank 项目并将其显示在“解决方案资源管理器”中,而且将在代码编辑器中打开 Program.cs 文件 。
注意
如果编辑器中未打开 Program.cs,请在“解决方案资源管理器”中双击文件 Program.cs 将其打开 。
将 Program.cs 的内容替换为以下定义 BankAccount 类的 C# 代码 :
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); } } }
通过在“解决方案资源管理器”中右键单击并选择“重命名”,将该文件重命名为 BankAccount.cs 。
在“构建”菜单中,单击“构建解决方案”(或按 Ctrl + Shift + B)。
现在具有可测试其方法的项目。 在本文中,测试侧重于 Debit
方法。 在从帐户提取资金时调用 Debit
方法。
创建单元测试项目
在“文件” 菜单上,选择“添加” >“新建项目” 。
提示
也可以右键单击“解决方案资源管理器”中的解决方案,然后依次选择“添加”>“新建项目” 。
在搜索框中键入“测试”,选择“C#”作为语言,然后为 .NET 模板选择 C#“MSTest 单元测试项目”模板,再单击“下一步”。
注意
在 Visual Studio 2019 版本 16.9 中,MSTest 项目模板是单元测试项目。
将项目命名为“BankTests”,然后单击“下一步” 。
选择建议的目标框架或 .NET 8,然后选择“创建”。
将“BankTests” 项目添加到“Bank” 解决方案。
在“BankTests” 项目中,添加对“Bank” 项目的引用。
在“解决方案资源管理器”中,选择“BankTests”项目下的“依赖项”,然后从右键单击菜单中选择“添加引用”(或“添加项目引用”)。
在“引用管理器”对话框中,展开“项目”,选择“解决方案”,然后选中“Bank”项 。
选择 “确定” 。
创建测试类
创建一个测试类来验证 BankAccount
类。 可以使用项目模板生成的 UnitTest1.cs 文件,但需为文件和类提供更具描述性的名称。
重命名文件和类
要重命名文件,在“解决方案资源管理器”中,从 BankTests 项目中选择 UnitTest1.cs 文件 。 在右键单击菜单中,选择“重命名”(或按 F2),再将文件重命名为 BankAccountTests.cs。
要重命名类,请将光标放置在代码编辑器的
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]
特性的其他方法。 可以从测试方法中调用这些其他的类和方法。
创建第一个测试方法
在此过程中,你将编写单元测试方法以验证 BankAccount
类的 Debit
方法的行为。
至少需要检查三种行为:
如果借方金额大于余额,该方法将引发 ArgumentOutOfRangeException 。
如果借方金额小于零,该方法会引发 ArgumentOutOfRangeException。
如果借方金额有效,该方法会从帐户余额中减去该借方金额。
提示
可以删除默认的 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.AreEqual
、Assert.IsTrue 等方法经常用于单元测试。 有关编写单元测试的更多概念性信息,请参阅编写测试。
测试方法要求
测试方法必须满足以下要求:
使用
[TestMethod]
特性进行修饰。它将返回
void
。它不能含有参数。
生成并运行测试
在“构建”菜单中,选择“构建解决方案”(或按 Ctrl + Shift + B)。
如果未打开“测试资源管理器”,请通过在顶部菜单栏中选择“测试”>“测试资源管理器”(或“测试”>“Windows”>“测试资源管理器”)将其打开(或按 Ctrl + E,T)。
选择“全部运行”以运行测试(或按 Ctrl + R,V)。
测试运行时,“测试资源管理器”窗口顶部的状态栏呈动态 。 测试运行结束时,如果测试方法全部通过,状态栏将变为绿色;如果有任何测试失败,状态栏将变为红色。
在这种情况下,测试失败。
在“测试资源管理器” 中选择该方法,可在窗口底部查看详细信息。
修复代码并重新运行测试
测试结果包含一条描述失败的消息。 可能需要向下钻取才能查看此消息。 对于 AreEqual
方法,该消息显示预计余额和实际余额。 虽然预计余额会减少,但余额中反而增加了取款金额。
单元测试已发现一个 bug:取款金额本应从帐户余额中减去 ,结果却添加 到帐户余额中。
更正 bug
若要更正该错误,请在 BankAccount.cs 文件中替换以下行 :
m_balance += amount;
替换为:
m_balance -= amount;
重新运行测试
在测试资源管理器中,选择“全部运行”以重新运行测试(或按 Ctrl + R,V)。 红色/绿色栏变为绿色,指示已通过测试。
使用单元测试以改进代码
本部分介绍了分析的迭代过程、单元测试开发和重构如何帮助你增加成品代码的可靠性和有效性。
分析问题
已创建测试方法,以确认是否在 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));
}
使用 ThrowsException 方法断言已引发正确的异常。 除非 ArgumentOutOfRangeException 已引发,否则该方法将导致测试失败。 如果在借方金额小于零时,临时修改测试方法以引发更通用的 ApplicationException,则测试将正确运行,即测试将失败。
若要测试提取金额大于余额的情况,执行以下操作:
新建一个名为
Debit_WhenAmountIsMoreThanBalance_ShouldThrowArgumentOutOfRange
的测试方法。将
Debit_WhenAmountIsLessThanZero_ShouldThrowArgumentOutOfRange
中的方法主体复制到新方法中。将
debitAmount
设置为比余额大的一个数字。
运行两个测试,并验证是否已通过这两个测试。
继续分析
可以进一步改进所测试的方法。 对于当前实现,我们没有办法知道哪个条件(amount > m_balance
或 amount < 0
)导致在测试期间引发异常。 我们只知道在方法中引发了一个 ArgumentOutOfRangeException
。 更理想的情况是,如果我们知道是 BankAccount.Debit
中的哪个条件导致引发异常(amount > m_balance
或 amount < 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);
}
重构测试方法
通过删除对 Assert.ThrowsException 的调用重构测试方法。 将对 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);
}
}
重测、重写和重新分析
目前,测试方法不会处理它原本应该处理的所有情况。 如果所测试的方法 Debit
在 debitAmount
大于余额(或小于零)时未能引发 ArgumentOutOfRangeException,则该测试方法通过。 这种方案不是很好,因为你希望在未引发异常的情况下使测试方法失败。
而此方案的结果是测试方法中出现 bug。 要解决该问题,在测试方法末尾添加 Assert.Fail 断言,处理未引发异常的情况。
重新运行测试表明,如果捕获到正确的异常,测试现将失败 。 catch
块捕获到该异常,但该方法继续执行,并在新的 Assert.Fail 断言处失败。 要解决此问题,在 catch
块中的 StringAssert
后添加 return
语句。 重新运行测试可确认已解决此问题。 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 命令行选项。