单元测试基础

本主题介绍了在 Visual Studio 测试资源管理器中编写和运行单元测试的基础知识。它包含下列部分:

单元测试概述

  • 快速入门

MyBank 解决方案示例

创建单元测试项目

编写你的测试

在资源管理器中运行测试

  • 从测试资源管理器工具栏运行并查看测试

  • 在每次生成后运行测试

  • 筛选和分组测试列表

调试单元测试

用于单元测试的其他工具

  • 从测试生成应用程序代码

  • 通过使用数据驱动的测试方法生成多个测试

  • 分析单元测试代码覆盖率

  • 使用 Microsoft Fake 隔离单元测试方法

单元测试概述

Visual Studio 测试资源管理器旨在为开发人员和团队提供支持,帮助其将单元测试纳入软件开发实践中。单元测试可帮助你通过验证应用程序代码执行你希望它执行的操作,确保你的程序的正确性。在单元测试中,你可以分析你的程序的功能,以发现可以作为单个单位测试的离散可测试行为。你使用单元测试框架创建这些行为的测试,并报告这些测试的结果。

当作为软件开发工作流的组成部分时,单元测试具有最大的影响。只要你编写了一个函数或其他应用程序代码块,你就可以创建单元测试用于验证对应于输入数据的标准、边界和不正确情况的代码的行为,而且用于验证代码所做的任何显式或隐式假设。在称为测试驱动开发的软件开发实践中,你需要在编写代码前创建单元测试,这样你可以使用单元测试作为功能的设计文档和功能规范。

测试资源管理器提供了一种灵活而高效的方法运行你的单元测试并在 Visual Studio 中查看其结果。Visual Studio 为托管和本机代码安装了 Microsoft 单元测试框架。测试资源管理器还可以运行第三方和开放源代码单元测试框架,它们实现了测试资源管理器外接程序接口。你可以通过 Visual Studio Extension Manager 和 Visual Studio 库添加其中许多框架。请参见如何:安装第三方单元测试框架

测试资源管理器视图可以显示所有测试,或仅显示已通过、失败、未运行或已跳过的测试。通过在全局级别的搜索框中的匹配文本或选择一个预定义的筛选器,你可以在任何视图中筛选测试。你可以在任何时间运行任何选定的测试。当你使用Visual Studio 旗舰版时,你可以在每次生成后自动运行测试。测试运行的结果立即显示在资源管理器窗口的顶部的通过/失败栏中。在你选择测试时,会显示测试方法结果的详细信息。

快速入门

关于直接进入编码的单元测试的简介,请参阅以下主题之一:

MyBank 解决方案示例

在本主题中,我们使用称为MyBank的虚构应用程序的开发作为示例。你不需要按照本主题中的说明的实际代码。测试方法用 C# 编写并使用 Microsoft 单元测试框架为托管代码进行呈现,但是,概念可以轻松地转到其他语言和框架中。

MyBank 解决方案

我们第一次尝试设计的MyBank应用程序包含表示个人帐户及其与银行交易的帐户组件,以及表示集合和管理单独帐户的功能的数据库组件。

我们创建包含两个项目的MyBank解决方案:

  • Accounts

  • BankDb

我们首次尝试设计Accounts的项目包含一个类来保存有关帐户的基本信息,以及指定任何类型的帐户的通用功能的接口的基本信息(如从该帐户存储和取出资产),以及从表示存款帐户的接口派生的类的基本信息。首先,我们通过创建以下源文件开始帐户项目:

  • AccountInfo.cs 定义帐户的基本信息。

  • IAccount.cs 为帐户定义一个标准 IAccount 接口,包括从一个帐户存款和收回资产和检索帐户余额的方法。

  • CheckingAccount.cs 包含 CheckingAccount 类,该类实现支票帐户的 IAccounts 接口。

我们根据经验可知,从活期存款中取款必须要确保提取的金额小于帐户余额。因此我们用检查这种情况的一种方法来重写CheckingAccount中的IAccount.Withdaw方法。该方法可能如下所示:

public void Withdraw(double amount)
{
    if(m_balance >= amount)
    {
        m_balance -= amount;
    }
    else
    {
        throw new ArgumentException(amount, "Withdrawal exceeds balance!")
    }
}

现在,我们有了一些代码,现在可以开始测试。

创建单元测试项目

单元测试项目通常会镜像单个代码项目的结构。在 MyBank 示例中,你将把名为AccountsTests和BankDbTests的两个单元测试项目添加到MyBanks解决方案中。测试项目名称是任意的,但采用标准命名约定是一个好主意。

若要向解决方案中添加单元测试项目:

  1. 文件菜单上,选择新建,然后选择项目(快捷键:Ctrl + Shift + N)。

  2. 在“新建项目”对话框中,展开已安装节点,选择你要用于测试项目的语言,然后选择测试

  3. 若要使用 Microsoft 单元测试框架之一,请从项目模板的列表选择单元测试项目。否则,请选择你想要使用的单元测试框架的项目模板。若要测试我们的示例中的Accounts项目,你需要命名该项目AccountsTests。

    警告说明警告

    并非所有第三方和开放源代码单元测试框架都提供 Visual Studio 项目模板。有关创建项目的信息,请参阅框架文档。

  4. 在你的单元测试项目中,将引用添加到所测试项目的代码中,在我们的示例中应添加到帐户项目中。

    若要创建代码项目的引用:

    1. 在解决方案资源管理器中选择项目。

    2. 项目菜单上,选择添加引用

    3. 在“引用管理器”对话框中,打开解决方案节点,然后选择项目。选择代码项目名称并关闭对话框。

每个单元测试项目包含类,用于镜像代码项目中类的名称。在我们的示例,AccountsTests项目将包含以下类:

  • AccountInfoTests 类包含 BankAccount 项目中用于 AccountInfo 类的单元测试方法

  • CheckingAccountTests 类包含用于 CheckingAccount 类的单元测试方法。

编写你的测试

你使用的单元测试框架和 Visual Studio IntelliSense 将指导你完成创建代码项目的单元测试。若要在测试资源管理器中运行,大多数框架要求你添加特定的属性来识别单元测试方法。框架还提供了一种方法,通常通过断言语句或方法属性,来指示测试方法是否已通过或失败。其他属性标识可选的安装方法,即在类初始化时和每个测试方法和每个拆卸方法之前的安装方法,这些拆卸方法在每个测试方法之后和类被销毁之前运行。

AAA(准备、执行、断言)模式是编写待测试方法的单元测试的常用方法。

  • 单元测试方法的准备部分初始化对象并设置传递给所测试方法的数据的值。

  • 执行部分调用具有准备参数的待测试方法。

  • 断言部分验证待测试方法的执行行为与预期相同。

若要测试我们的示例中的CheckingAccount.Withdraw方法,我们可以编写两个测试:一个验证方法的标准行为,另一个验证取款而不只是结算将失败。在CheckingAccountTests类中,我们将添加以下方法:

[TestMethod]
public void Withdraw_ValidAmount_ChangesBalance()
{
    // arrange
    double currentBalance = 10.0;
    double withdrawal = 1.0;
    double expected = 9.0;
    var account = new CheckingAccount("JohnDoe", currentBalance);
    // act
    account.Withdraw(withdrawal);
    double actual = account.Balance;
    // assert
    Assert.AreEqual(expected, actual);
}

[TestMethod]
[ExpectedException(typeof(ArgumentException))]
public void Withdraw_AmountMoreThanBalance_Throws()
{
    // arrange
    var account = new CheckingAccount("John Doe", 10.0);
    // act
    account.Withdraw(1.0);
    // assert is handled by the ExpectedException
}

请注意,Withdraw_ValidAmount_ChangesBalance使用显式Assert语句确定测试方法是通过还是失败,而Withdraw_AmountMoreThanBalance_Throws使用ExpectedException属性确定测试方法的成功。在底层,单元测试框架使用 try/catch 语句包装测试方法。在大多数情况下,只要捕获了一个异常,则测试方法就会失败,并且该异常将被忽略。如果指定的异常被抛弃,ExpectedException属性将导致该测试方法通过。

有关 Microsoft 单元测试框架的详细信息,请参阅以下主题之一:

设置超时

若要在单个测试方法上设置超时:

[TestMethod]
[Timeout(2000)]  // Milliseconds
public void My_Test()
{ ...
}

若要将超时设置为允许的最大值:

[TestMethod]
[Timeout(TestTimeout.Infinite)]  // Milliseconds
public void My_Test ()
{ ...
}

在资源管理器中运行测试

在生成测试项目时,测试将出现在测试资源管理器中。如果测试资源管理器不可见,请选择 Visual Studio 菜单上的测试,选择Windows,然后选择测试资源管理器

单元测试资源管理器

当你运行、编写并重新运行测试时,测试资源管理器的默认视图将显示失败的测试通过的测试跳过测试未运行的测试组中的结果。你可以选择组标题打开在该组中显示所有这些测试的视图。

从测试资源管理器工具栏运行并查看测试

测试资源管理器工具栏可帮助你发现、组织和运行你感兴趣的测试。

从测试资源管理器工具栏运行测试

你可以选择运行全部来运行所有测试,或选择运行来选择要运行的测试的子集。运行一组测试后,测试运行的摘要将出现在测试资源管理器窗口的底部。选择一个测试以在底部窗格中查看该测试的详细信息。从上下文菜单选择“打开测试”(键盘:按 F12),以显示所选测试的源代码。

在每次生成后运行测试

警告说明警告

只有 Visual Studio Ultimate 支持每次生成后运行单元测试。

生成后运行

若要在每个本地生成后运行单元测试,请在标准菜单上选择测试,然后在测试资源管理器工具栏上选择生成后运行测试

筛选和分组测试列表

当你有大量的测试时,你可以在测试资源管理器搜索框中键入,以按指定的字符串筛选列表。你可以通过从筛选器列表中选择以更多地限制你的筛选器事件。

搜索筛选器类别

测试资源管理器的分组按钮

若要按类别分组测试,请选择分组依据按钮。

有关详细信息,请参阅用测试资源管理器运行单元测试

调试单元测试

可以使用测试资源管理器为你的测试启动调试会话。使用 Visual Studio 调试程序无缝地逐句通过代码将使你在单元测试和所测试项目之间来回反复。要开始调试:

  1. 在 Visual Studio 编辑器中,在你想要调试的一个或多个测试方法中设置断点。

    说明说明

    因为测试方法可以按任何顺序运行,请在你想要调试的所有测试方法中设置断点。

  2. 在测试资源管理器中,选择测试方法,然后从快捷菜单选择调试选定的测试

有关该调试器的更多信息,请参见使用 Visual Studio 进行调试

用于单元测试的其他工具

从测试生成应用程序代码

在编写你的项目代码之前,如果你正在编写你的测试,你可以使用 IntelliSense 在你的项目代码中生成类和方法。编写你想要生成的调用类或方法的测试方法中的语句,然后打开调用下面的 IntelliSense 菜单。如果调用是到新类的构造函数中,请从菜单选择生成新类型并按照向导在你的代码项目中插入此类。如果调用到方法,请从 IntelliSense 菜单选择生成新方法

生成方法存根 Intellisense 菜单

通过使用数据驱动的测试方法生成多个测试

说明说明

这些过程仅适用于你使用的 Microsoft 单元测试框架为托管代码编写的测试方法。如果你在使用不同的框架,请查阅框架文档,获取等效的功能。

数据驱动的测试方法使你可以验证一个单元测试方法中的值的范围。若要创建数据驱动的单元测试方法,请用DataSource属性修饰方法,它指定的数据源和表包含你想要测试的变量值。在方法体中,你可以使用TestContext.DataRow[ColumnName]索引器将行值分配给变量。

例如,假定我们将不必要的方法添加到名为AddIntegerHelperd CheckingAccount类中。AddIntegerHelper 添加两个整数。

若要创建数据驱动的测试AddIntegerHelper方法中,我们首先要创建名为AccountsTest.accdb的 Access 数据库和名为AddIntegerHelperData的表。AddIntegerHelperData表定义了指定要添加的第一个和第二个操作数的列和指定所需结果的列。我们使用适当的值填充行数。

    [DataSource(
        @"Provider=Microsoft.ACE.OLEDB.12.0;Data Source=C:\Projects\MyBank\TestData\AccountsTest.accdb", 
        "AddIntegerHelperData"
    )]
    [TestMethod()]
    public void AddIntegerHelper_DataDrivenValues_AllShouldPass()
    {
        var target = new CheckingAccount();
        int x = Convert.ToInt32(TestContext.DataRow["FirstNumber"]);
        int y = Convert.ToInt32(TestContext.DataRow["SecondNumber"]); 
        int expected = Convert.ToInt32(TestContext.DataRow["Sum"]);
        int actual = target.AddIntegerHelper(x, y);
        Assert.AreEqual(expected, actual);
    }

特性化的方法将为表中的每一行运行一次。如果任何迭代失败,测试资源管理器将报告方法的测试失败。该方法的测试结果详细信息窗格显示每行数据的通过/失败状态方法。

有关详细信息,请参阅如何:创建数据驱动的单元测试

分析单元测试代码覆盖率

说明说明

单元测试代码覆盖率可用于本机和托管语言以及可由单元测试框架运行的所有单元测试框架。

你可以使用 Visual Studio 代码覆盖率工具确定你的单元测试实际测试的产品代码量。你可以在选定的测试上或解决方案中的所有测试上运行代码覆盖率。代码覆盖率结果窗口显示行、函数、类、命名空间和模块执行的产品代码块的百分比。

若要在解决方案中为测试方法运行代码覆盖率

  1. 在 Visual Studio 菜单上选择测试,然后选择分析代码覆盖率

  2. 选择以下命令之一:

    1. 选定的测试运行你在测试资源管理器中选择的测试方法。

    2. 所有测试在解决方案中运行所有测试方法。

覆盖率结果将显示在代码覆盖率结果窗口中。

代码覆盖率结果

有关详细信息,请参阅使用代码覆盖率确定所测试的代码量

使用 Microsoft Fake 隔离单元测试方法

说明说明

Microsoft Fakes 仅在 Visual Studio Ultimate 中可用。Microsoft Fakes 只能用于你使用单元测试框架为托管代码编写的测试方法。

问题

当所测试的方法调用引入外部依赖关系的函数时,单元测试方法的重点放在验证函数中可能很难编写的内部代码上。例如,CheckingAccount示例类的方法可能调用到BankDb组件来更新主数据库。我们可以重构CheckingAccount类使之与以下类似:

class CheckingAccount : IAccount
{
    public CheckingAccount(customerName, double startingBalance, IBankDb bankDb)
    {
        m_bankDb = bankDb;
        // set up account
    }

    public void Withdraw(double amount)
    {
        if(m_balance >= amount)
        {
            m_balance = m_MyBankDb.Withdraw(m_accountInfo.ID, amount);
        }
        else
        {
            throw new ArgumentException(amount, "Withdrawal exceeds balance!")
        }
    }

    private IBankDb m_bankDb = null;
    // ...

此CheckingAccount.Withdraw方法的单元测试由于调用到m_bankDb.Withdraw导致的问题现在可能会失败。数据库或网络连接可能会丢失,或数据库上的权限可能出错。m_bankDB.Withdraw调用中的失败将导致测试失败,因为它与其内部代码不相关。

Microsoft Fakes 解决方案

Microsoft Fakes 创建包含类和方法的程序集,你可以使用这些类和方法替代导致依赖关系的单元测试中的类。生成的 Fakes 模块中的替代类为目标组件中每个公共方法声明了一个方法和委托。在测试方法中,你可以实现委托,以在你想要测试的方法中创建依赖项调用的具体行为。

在本示例中,我们可以为BankDb项目创建 Fakes 程序集,然后使用 Fakes 生成的和IBankDb接口派生的StubIBankDb类,删除由数据库交互引起的不确定性。Withdraw_ValidAmount_ChangesBalance测试方法的已修改版本将如下所示:

[TestMethod]
public void Withdraw_ValidAmount_ChangesBalance()
{
    // arrange
    double currentBalance = 10.0;
    double withdrawal = 1.0;
    double expected = 9.0;

    // set up the Fakes object and delegate
    var stubBankDb = new MyBank.Stubs.StubIBankDb();
    stubBankDb.WithdrawDoubleDouble = (id, amount) => { return 9.0; }
    var account = new CheckingAccount("JohnDoe", currentBalance, stubBankDb);

    // act
    account.Withdraw(withdrawal);
    double actual = account.Balance;

    // assert
    Assert.AreEqual(expected, actual);
}

测试方法中的此行:

stubBankDb.WithdrawDoubleDouble = (id, amount) => { return 9.0; }

使用 lamba 表达式为Withdraw方法实现 Fakes 委托。stubBankDb.Withdraw方法调用委托,并因此始终返回指定的量,使测试方法能够可靠地验证Accounts方法的行为。

有关 Microsoft Fakes 的详细信息

Microsoft Fakes 使用两种方法创建替代类:

  1. 存根 (stub)生成从目标依赖关系类的父接口派生的替代类。可以将存根 (stub) 方法替换为目标类的公共虚拟方法。

  2. 填充码使用运行时间检测将调用转移到目标方法中,以用填充码方法替换非虚拟方法。

通过这两种方法,你可以使用对依赖关系方法的调用所生成的委托,指定测试方法中所需的行为。

有关详细信息,请参阅用 Microsoft Fakes 隔离测试代码