销售电话: 1-800-867-1380

有关如何有效测试 Azure 解决方案的指导

更新时间: 2014年6月

作者:Suren Machiraju

审阅者:Jaime Alva Bravo 和 Steve Wilkins

在设计、编码并部署 Microsoft Azure 解决方案后,你有时会发现它无法正常工作。本文提供有关如何在整个软件开发生命周期中测试 Microsoft Azure 应用程序的指导。测试范围包括业务逻辑和综合性的端到端方案测试。本文介绍如何:

  • 为业务逻辑组件开发单元测试,同时消除对 Microsoft Azure 组件的依赖关系。

  • 设计集成端到端测试。

  • 消除每次运行测试时设置、初始化、清理和拆卸 Microsoft Azure 服务资源(例如队列)所造成的系统开销。

  • 避免构建 ACS 命名空间、Service Bus 队列和其他资源的重复基础结构。

此外,本文还概述了用于测试 Microsoft Azure 应用程序的各种技术和方法。

我们对本文做了以下新的更改:

  1. 使用 Visual Studio 2013。

  2. Visual Studio 2013 中的 Microsoft Fake 取代了 Moles。我们在文章使用 Microsoft Fake 隔离测试中的代码中介绍了此功能。

  3. 在 Visual Studio 2013 中,现已提供新版本的代码覆盖率功能,可以直接从测试资源管理器调用该功能。有关说明,请阅读文章使用代码覆盖率确定正在测试的代码量

  4. Pex 团队现已发布名为 Code Digger 的 Pex 轻型版本。有关说明,请参阅文章 Microsoft Code Digger

  5. 从 Visual Studio 2012 开始,我们不再建议测试私有方法。有关说明,请阅读我采用的单元测试私有方法

有两种类型的测试:

  • 单元测试是范围很窄的测试,执行单项特定功能。我们将这些测试称为“测试中的代码”,简称 CUT。你必须删除 CUT 所需的任何依赖关系。

  • 集成测试是范围更广泛的测试,同时执行多项功能。很多情况下,这些测试类似于单元测试,但它们覆盖多个功能区域,包括多个依赖关系。

整体而言,这些测试的重点是创建和使用测试替身。我们使用以下类型的测试替身:

  • Fake 是模拟对象,用于实现与其代表的对象相同的接口。Fake 返回预定义的响应。一个 Fake 包含一组方法存根,用来替代你通过编程生成的对象。

  • 存根模拟软件对象的行为。

  • 填充码允许你将代码与并非解决方案一部分的程序集相隔离。它们还可将解决方案的各个组件相互隔离。

执行之后,这些测试可以验证状态和行为。例如,状态包括调用方法并返回特定值之后的结果。按特定顺序或特定次数调用方法就是行为的一个示例。

单元测试的主要目的之一是消除依赖关系。对于 Azure 框架,这些依赖关系包括以下内容:

  • Service Bus 队列。

  • 访问控制服务。

  • 缓存。

  • Azure 表、Blob 和队列。

  • Azure SQL Database。

  • Azure 驱动器(以前称为云驱动器)。

  • 其他 Web 服务。

为 Azure 应用程序生成测试时,我们将替换这些依赖关系,将测试的重点放在执行逻辑上。

在本文中讨论的 Service Bus 队列示例(包括工具和技术)也适用于其他所有依赖关系。

若要为你的 Microsoft Azure 应用程序实施测试框架,你需要:

  • 一个单元测试框架,用于定义和运行测试。

  • 一个模拟框架,用于帮助隔离依赖关系,并构建范围较窄的单元测试。

  • 帮助你自动生成单元测试以提高代码覆盖率的工具。

  • 其他框架,可以通过充分利用依赖关系注入和应用控制反向模式 (IoC),帮助你完成可测试的设计。

Visual Studio 包括一个名为 MS Test 的命令行实用工具,用于执行在 Visual Studio 中创建的单元测试。Visual Studio 还包括一组项目模板和项模板用于支持测试。你通常会创建一个测试项目,然后添加类(称为测试装置),这些类使用 [TestClass] 属性修饰。这些类包含使用 [TestMethod] 属性修饰的方法。在 MS Test 中,Visual Studio 中的各个窗口允许你执行在项目中定义的单元测试。执行单元测试之后,你还可以查看结果。

note备注
Visual Studio 2013 Express、Professional 和 Test Professional 版本不包含 MS Test。

在 MS Test 中,单元测试遵循 AAA 模式 - 即 Arrange(安排)、Act(执行)和 Assert(断言)。

  • 安排 - 构建所有必备的对象、配置,以及 CUT 所需的所有其他前提条件和输入。

  • 执行 - 对代码执行实际的窄范围测试。

  • 断言 - 确认期望的结果已发生。

MS Test 框架库包括 PrivateObjectPrivateType 帮助程序类。这些类使用了反射,方便从单元测试代码内部调用非公共实例成员或静态成员。

Visual Studio Premium 和 Ultimate 版本包含了增强型单元测试工具。这些工具与 MS Test 相集成。使用这些工具还可以分析执行单元测试的代码量。此外,这些工具还可对源代码进行颜色编码以指示覆盖率。这项功能称为代码覆盖率

单元测试的一个目标是在隔离条件下进行测试。但是,你经常无法在隔离环境下测试代码。有时,编写的代码没有可测试性。重新编写代码是非常困难的,因为它依赖于不易隔离的其他库。例如,与外部环境交互的代码无法轻易隔离。模拟框架可帮助你隔离两种类型的依赖关系。

本文末尾的链接部分提供了你可以考虑使用的模拟框架列表。本文重点介绍如何使用 Microsoft Fake。

Microsoft Fake 可以通过将应用程序的其他部分替换为存根或填充码来帮助你隔离要测试的代码。这些是受测试控制的少量代码。通过隔离要测试的代码,你可在测试失败的情况下,确认导致失败的确切位置。利用存根和填充码,即使在应用程序的其他部分尚未运行的情况下,你也可以测试代码。

Fake 分为两种形式:

  • 存根使用可实现相同接口的少量替代来替换一个类。若要使用存根,应用程序的设计必须让每个组件仅依赖于接口,而不依赖于其他组件。我们所说的“组件”指一个类或一组类,这些类共同设计和更新,通常包含在一个程序集中。

  • 填充码可在运行时修改已编译的应用程序代码。应用程序不会执行指定的方法调用,而是运行你的测试提供的填充码。你可以使用填充码来替换对无法修改的程序集(例如 .NET 程序集)的调用。

Code Digger 可通过 .NET 代码分析可能的执行路径。其结果是一个表,其中的每行都显示代码的独特行为。该表可帮助你了解代码的行为,还可以揭示隐藏的 Bug。

若要在 Visual Studio 编辑器中分析代码,请使用新的上下文菜单项“生成输入/输出表”来调用 Code Digger。Code Digger 可计算并显示输入-输出对。Code Digger 可以系统地查找 Bug、异常和断言失败。

默认情况下,Code Digger 仅适用于位于可移植类库中的公共 .NET 代码。在本文的后面,我们将讨论如何配置 Code Digger 以浏览其他 .NET 项目。

Code Digger 使用 Pex 引擎和 Microsoft 研究院的 Z3 约束规划求解来系统地分析代码中的所有分支。Code Digger 将尝试生成测试套件,以实现较高的代码覆盖率。

你可将 Microsoft Unity 用于可扩展依赖关系注入 (DI) 和控制反向 (IoC) 容器。它支持侦听、构造函数注入、属性注入和方法调用注入。Microsoft Unity 和类似工具可帮助你构建可测试的设计,让你能够在应用程序的所有级别上插入依赖关系。(假设你在设计和构建应用程序时考虑到了依赖关系注入和上述某一种框架。)

这些框架非常有利于编写可测试代码,从而最终编写出高质量代码。但是,在前期设计要求方面,它们可能是非常严格的。我们在本文中不讨论 DI 和 IoC 容器。

在本部分,我们将介绍一种解决方案,该解决方案包含一个托管在 Web 角色中的网站。该网站将消息推送到队列。然后,辅助角色将处理来自该队列的消息。我们将讨论上述所有三个方面的测试。

假设你有一个可创建订单的网站,Service Bus 队列将创建一个队列用于处理这些订单。网页类似于图 1:

图 1

当用户单击“创建”时,Service Bus 队列将新订单发布到关联控制器上的 Create 操作。该操作通过以下方式实现:

note备注
MicrosoftAzureQueue 类是一个包装类,它使用 Service Bus .NET API(例如 MessageSender 和 MessageReceiver)来与 Service Bus 队列交互。

private IMicrosoftAzureQueue queue;
public OrderController()
{
    queue = new MicrosoftAzureQueue();
}
public OrderController(IMicrosoftAzureQueue queue)
{
    this.queue = queue;
}
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create([Bind(Include = "OrderId,Description")] Order order)
{
    try
    {
        if (ModelState.IsValid)
        {
            string connectionString = CloudConfigurationManager.GetSetting("Microsoft.ServiceBus.ConnectionString");
            string queueName = "ProcessingQueue";
            queue.InitializeFromConnectionString(connectionString, queueName);
            queue.Send(order);
        }
        return View("OrderCreated");
    }
    catch (Exception ex)
    {
        Trace.TraceError(ex.Message);
        return View("Error");
    }            
}

请注意,代码从 CloudConfigurationManager 检索配置设置,并将包含订单的消息发送到队列。另请注意,Create 操作使用以下方法:

  • InitializeFromConnectionString (ConnectionString string, QueueName string)

  • Send (MicrosoftAzureQueue class)

我们希望通过使用 Fake 来控制方法的行为并消除对实际环境的依赖关系,从而为这些方法构建 detour。使用 Fake 无需在 Azure 仿真程序中运行测试,也无需调用 Service Bus 队列。我们在控制器上执行 Create 操作,以验证订单输入是否为发送到队列的那一个输入。Send 方法检查该输入是否具有与操作输入相关的订单 ID 和说明。然后,检查 OrderCreated 视图是否显示为结果。

使用 Fake 为上述实现构建单元测试非常简单。在测试项目内,右键单击包含你要模拟的类型的程序集,然后选择“添加 Fakes 程序集”

在本例中,我们选择“Microsoft.ServiceBus”,然后选择“添加 Fake 程序集”。名为“Microsoft.ServiceBus.Fakes”的 XML 文件被添加到测试项目中。针对 Microsoft.WindowsAzure.Configuration 程序集重复该操作。

构建测试项目时,引用将被添加到自动生成的这些程序集的模拟版本。在本例中,生成的程序集为“Microsoft.ServiceBus.Fakes”和“Microsoft.WindowsAzure.Configuration”。

创建单元测试方法并应用 [TestCategory("With fakes")] 属性。在单元测试中,可以使用填充码将应用程序的各个部分相互隔离。

使用填充码将应用程序与其他程序集相隔离以进行单元测试

填充码是 Microsoft Fakes Framework 提供的两种技术之一,让你能够轻松地将测试中的组件与环境相隔离。填充码将对特定方法的调用转至作为测试的一部分编写的代码。很多方法返回不同的结果,具体取决于外部条件。但是,填充码处于测试的控制之下,可在每次调用时返回一致的结果。这样,你便可以更轻松地编写测试。使用填充码可将代码与并非解决方案一部分的程序集相隔离。若要将解决方案的各个组件相互隔离,我们建议你使用存根。

使用存根将应用程序的各个部分相隔离以进行单元测试

存根是 Microsoft Fakes Framework 提供的两种技术之一,让你能够轻松地将要测试的组件与它所调用的其他组件相隔离。存根是一小段代码,可在测试过程中替代其他组件。使用存根的优点是它返回一致的结果,让你能够更加轻松地编写测试。此外,即使其他组件尚未运行,你也可以运行测试。

在我们的测试案例中,我们将填充码用于 Azure 程序集中的 CloudConfigurationManager 和 BrokeredMessage。我们会将存根用于解决方案中的类 MicrosoftAzureQueue。

[TestMethod]
[TestCategory("With fakes")]
public void Test_Home_CreateOrder()
{
    // Shims can be used only in a ShimsContext
    using (ShimsContext.Create())
    {
        // Arrange
        // Use shim for CloudConfigurationManager.GetSetting
        Microsoft.WindowsAzure.Fakes.ShimCloudConfigurationManager.GetSettingString = (key) =>
        {
            return "mockedSettingValue";
        };
                
        // Create the fake queue:
        // In the completed application, queue would be a real one:
        bool wasCreateFromConnString = false;
        Order orderSent = null;
        IMicrosoftAzureQueue queue =
                new OrderWebRole.Queue.Fakes.StubIMicrosoftAzureQueue() // Generated by Fakes.
                {
                    // Define each method:
                    // Name is original name + parameter types:
                    InitializeFromConnectionStringStringString = (connectionString, queueName) => {
                        wasCreateFromConnString = true;
                    },
                    SendOrder = (order) => {
                    orderSent = order;
                    }
                };

        // Component under test
        OrderController controller = new OrderController(queue);

        // Act
        Order inputOrder = new Order()
        {
            OrderId = System.Guid.NewGuid(),
            Description = "A mock order"
        };
        ViewResult result = controller.Create(inputOrder) as ViewResult;

        //Assert
        Assert.IsTrue(wasCreateFromConnString);
        Assert.AreEqual("OrderCreated", result.ViewName);
        Assert.IsNotNull(orderSent);
        Assert.AreEqual(inputOrder.OrderId, orderSent.OrderId);
        Assert.AreEqual(inputOrder.Description, orderSent.Description);
    }
}

Web 角色负责将订单添加到队列。现在,请考虑如何测试辅助角色,它将从 Service Bus 队列检索订单并对订单进行处理。辅助角色中的 Run 方法定期轮询队列中的订单,并对订单进行处理。

private IMicrosoftAzureQueue queue;
public WorkerRole()
{
    queue = new MicrosoftAzureQueue();
}

public WorkerRole(IMicrosoftAzureQueue queue)
{
    this.queue = queue;
}
public override void Run()
{
    try
    {
        string connectionString = CloudConfigurationManager.GetSetting("Microsoft.ServiceBus.ConnectionString");
        string queueName = "ProcessingQueue";
               
        queue.InitializeFromConnectionString(connectionString, queueName);
            
        queue.CreateQueueIfNotExists();
              
        while (true)
        {
            Thread.Sleep(2000);
            //Retrieve order from Service Bus Queue  
            TryProcessOrder(queue);
        }
    }
    catch (Exception ex)
    {
        if (queue != null)
            queue.Close();
        System.Diagnostics.Trace.TraceError(ex.Message);
    }
}

我们希望通过测试来验证例程是否能够正常检索消息。以下是针对辅助角色的 Run 方法的完整单元测试。

[TestMethod]
public void Test_WorkerRole_Run()
{
    // Shims can be used only in a ShimsContext:
    using (ShimsContext.Create())
    {
        Microsoft.WindowsAzure.Fakes.ShimCloudConfigurationManager.GetSettingString = (key) =>
        {
            return "mockedSettingValue";
        };

        // Arrange 
        bool wasEnsureQueueExistsCalled = false;
        int numCallsToEnsureQueueExists = 0;

        // Create the fake queue:
        // In the completed application, queue would be a real one:
        bool wasConnectionClosedCalled = false;
        bool wasCreateFromConnString = false;
        bool wasReceiveCalled = false;
        int numCallsToReceive = 0;

        bool wasCompleteCalled = false;
        int numCallsToComplete = 0;
        IMicrosoftAzureQueue queue =
                new OrderWebRole.Queue.Fakes.StubIMicrosoftAzureQueue() // Generated by Fakes.
                {

                    // Define each method:
                    // Name is original name + parameter types:
                    InitializeFromConnectionStringStringString = (connectionString, queueName) =>
                    {
                        wasCreateFromConnString = true;
                    },
                    CreateQueueIfNotExists = () =>
                    {
                        wasEnsureQueueExistsCalled = true;
                        numCallsToEnsureQueueExists++;
                    },
                    Receive = () =>
                {
                    wasReceiveCalled = true;
                    if (numCallsToReceive >= 3) throw new Exception("Aborting Run");
                    numCallsToReceive++;
                    Order inputOrder = new Order()
                    {
                        OrderId = System.Guid.NewGuid(),
                        Description = "A mock order"
                    };
                    return new BrokeredMessage(inputOrder);
                },
                    Close = () =>
                    {
                        wasConnectionClosedCalled = true;
                    }

                };


        Microsoft.ServiceBus.Messaging.Fakes.ShimBrokeredMessage.AllInstances.Complete = (message) =>
        {
            wasCompleteCalled = true;
            numCallsToComplete++;
        };

        WorkerRole workerRole = new WorkerRole(queue);

        //Act
        workerRole.Run();

        //Assert
        Assert.IsTrue(wasCreateFromConnString);
        Assert.IsTrue(wasConnectionClosedCalled);
        Assert.IsTrue(wasEnsureQueueExistsCalled);
        Assert.IsTrue(wasReceiveCalled);
        Assert.AreEqual(1, numCallsToEnsureQueueExists);
        Assert.IsTrue(numCallsToReceive > 0);
        Assert.IsTrue(wasCompleteCalled);
        Assert.IsTrue(numCallsToComplete > 0);
        Assert.AreEqual(numCallsToReceive, numCallsToComplete);

    }
}

你应该设置所生成 Fake 类型的 AllInstances 属性的委托。使用委托时,你创建的实际 Fake 类型的任何实例都可以绕过已为其定义委托的任何方法。

在本例中,我们希望使用原始实例的 Run 方法,但为 CreateQueueTryProcessOrder 实例方法提供 detour。在代码中,我们引发了异常,因此我们退出了 Run 方法在预定时间维持的无限循环。

你可能会问,为什么不直接使用 Service Bus SDK 中的 MessageSender/MessageReceiver 及相关类,而要插入帮助程序类型呢?若要完全隔离代码,使它不会调用实际的 Service Bus,我们有两种选择:

  • 编写从 Microsoft.ServiceBus 命名空间中的抽象类继承的 Fake。

  • 让 Fake 为它们创建 mock 类型。

两种方法的问题在于复杂性。这两种方法最终都会反射到 TokenProviderQueueClient 等类。反射导致以下问题:

  • 你必须从这些抽象类型创建派生类型,公开它们的所有必需重写。

  • 你必须公开这些类的实际版本真正依赖的内部类型。

  • 对于内部类型,你必须通过合理的方式重新创建它们的构造函数或工厂方法,以消除对实际 Service Bus 的依赖关系。

更好的选择是插入你自己的帮助程序类型。这样即可执行 mock 和 detour,并将代码与实际 Service Bus 相隔离。

为了分析这些单元测试已经验证了哪些代码,我们可以查看代码覆盖率数据。如果使用 MS Test 运行以上两种单元测试,我们可以看到测试通过。我们还可在“测试资源管理器”对话框中查看相关的运行详细信息。

图 2

你可以通过测试资源管理器为测试运行代码覆盖率功能。右键单击测试,然后选择“分析所选测试的代码覆盖率”。结果将显示在“代码覆盖率结果”窗口中。若要激活代码覆盖率的数据收集,请配置“解决方案项”下的 Local.testsettings 文件。打开此文件将启动“测试设置”编辑器。

如果你的解决方案不包括 Local.testsettings 文件,请使用以下过程将其添加到解决方案。

  1. 单击“添加新项”按钮。

  2. 选择“测试”,然后单击“测试设置”


    图 3

  3. 单击“数据和诊断”选项卡,然后选中“代码覆盖率”行右侧的复选框。

  4. 接下来,单击“配置”按钮 。

  5. “代码覆盖率详细信息”窗口中,选择你要测试的所有程序集,然后单击“确定”


    图 4

  6. 若要关闭“测试设置”编辑器,请单击“应用并关闭”

  7. 重新运行测试,然后单击“代码覆盖率”按钮。代码覆盖率结果应该如图 5 所示。


    图 5

  8. 单击“显示代码覆盖率着色”图标,然后向下导航到网格中的某个方法。

  9. 双击该方法。该方法的源代码将以某种颜色显示,该颜色指示已测试的区域。绿色指示代码已测试。灰色指示代码仅经过部分测试或尚未测试。


    图 6

手动创建单元测试是非常可取的做法,但 Code Digger 还可以帮助你通过智能方式升级单元测试。实现这一目标的方法是尝试你可能还没有考虑的参数值。安装 Code Digger 之后,可通过在代码编辑器中右键单击方法并选择“生成输入/输出表”来浏览该方法。

图 7

稍后,Code Digger 将完成其处理,结果也将稳定。例如,图 8 显示了对辅助角色的 TryProcessOrder 方法运行 Code Digger 的结果。请注意,Code Digger 能够创建一个导致异常的测试。Code Digger 还显示它创建的用于生成该异常(队列参数的 null 值)的输入,这一点更加重要。

图 8

本文是否对您有所帮助?
(1500 个剩余字符)
感谢您的反馈
Microsoft 正在进行一项网上调查,以了解您对 MSDN 网站的意见。 如果您选择参加,我们将会在您离开 MSDN 网站时向您显示该网上调查。

是否要参加?
显示:
© 2014 Microsoft