练习 - 单元测试 Azure Functions

已完成

单元测试是敏捷方法的基本部分。 Visual Studio 提供测试项目模板。 使用此模板为应用程序创建单元测试,可将相同的技术应用于 Azure Functions 测试。

在奢侈手表联机网站方案中,开发团队制定了一项策略,即在单元测试中实现至少 80% 的代码覆盖率。 你想要为 Azure Functions 实现相同的策略。

在此处,你将了解如何通过 Visual Studio 使用 xUnit 测试框架来测试 Azure Functions。

创建单元测试项目

第一步是创建一个包含单元测试的项目,并将其添加到包含 Azure 函数应用的解决方案中。 使用以下步骤创建单元测试项目以测试 WatchInfo 函数。

  1. 在 Visual Studio 的“解决方案资源管理器”窗口中,右键单击“WatchPortalFunction”解决方案,选择“添加”,然后选择“新建项目”。

    “解决方案资源管理器”的屏幕截图,其中显示“将新项目添加到解决方案”命令。

  2. 在“添加新项目”窗口中,向下滚动,选择“xUnit 测试项目”C#+ 图标模板,然后选择“下一步”。

    “添加新项目”窗口的屏幕截图。选择了 xUnit 测试项目模板。

  3. 显示“配置新项目”窗口。 在“项目名称”字段中,输入“WatchFunctionsTests”。 选择“位置”字段旁边的浏览图标,然后选择“WatchPortalFunction”文件夹。

  4. 选择“下一页”。 将显示“其他信息”窗口。

  5. 在“目标框架”下。 接受“.NET 6.0 (长期支持)”的默认值。

  6. 选择创建

  7. 添加项目后,右键单击“解决方案资源管理器”窗口中的“WatchFunctionTests”项目,然后选择“管理 NuGet 包”

  8. 在“NuGet:WatchFunctionTests”窗口中,选择“浏览”选项卡。在“搜索”框中,输入“Microsoft.AspNetCore.Mvc”。 选择“Microsoft.AspNetCore.Mvc”包,然后选择“安装”。

    “NuGet 包管理器”窗口的屏幕截图。用户正在安装 Microsoft.AspNetCore.Mvc 包。

    注意

    测试项目将创建一个模拟 HTTP 环境。 执行此操作所需的类位于 Microsoft.AspNetCore.Mvc 包中。

  9. 包安装期间,请等待。 如果出现“预览更改”消息框,请选择“确定”。 在“许可证接受”消息框中,选择“我接受”。

  10. 添加软件包后,在“解决方案资源管理器”窗口的“WatchFunctionsTests”项目下,右键单击“UnitTest1.cs”文件,然后选择“重命名”。 将文件名更改为“WatchFunctionUnitTests.cs”。 在出现的消息框中,选择“是”,将“UnitTest1”的所有引用重命名为“WatchFunctionUnitTests”。

  11. 在“解决方案资源管理器”窗口中,在“WatchFunctionsTests”项目下,单击“依赖项”,然后选择“添加项目引用”。

  12. 在“引用管理器”窗口中,选择“WatchPortalFunction”项目,然后选择“确定”。

为 WatchInfo 函数添加单元测试

现可将单元测试添加到测试项目。 在奢侈手表方案中,想要确保 WatchInfo 函数在请求的查询字符串中提供模型时始终返回正确响应,并且如果查询字符串为空或不包含 model 参数时返回错误响应。

将向 WatchFunctionsTests 添加一对 Fact 测试以验证此行为

  1. 在“解决方案资源管理器”窗口中,双击“WatchFunctionUnitTests.cs”文件以在代码窗口中显示 WatchPortalFunction。

  2. 将以下 using 指令添加到文件顶部的列表中。

    using Microsoft.AspNetCore.Http;
    using Microsoft.AspNetCore.Http.Internal;
    using Microsoft.AspNetCore.Mvc;
    using Microsoft.Extensions.Primitives;
    using Microsoft.Extensions.Logging.Abstractions;
    
  3. 将 Test1 方法的名称更改为 TestWatchFunctionSuccess。

  4. 在 TestWatchFunctionSuccess 方法的正文中,添加以下代码。 此语句创建模拟 HTTP 上下文和 HTTP 请求。 该请求包括一个包含 model 参数的查询字符串,该参数设置为 abc

    var queryStringValue = "abc";
    var request = new DefaultHttpRequest(new DefaultHttpContext())
    {
        Query = new QueryCollection
        (
            new System.Collections.Generic.Dictionary<string, StringValues>()
            {
                { "model", queryStringValue }
            }
        )
    };
    
  5. 在方法中添加以下语句。 该语句创建一个虚拟记录器。

    var logger = NullLoggerFactory.Instance.CreateLogger("Null Logger");
    
  6. 将以下代码添加到方法中。 这些语句调用 WatchInfo 函数,作为参数传入虚拟请求和记录器。

    var response = WatchPortalFunction.WatchInfo.Run(request, logger);
    response.Wait();
    
  7. 将以下代码添加到方法中。 此代码检查函数的响应是否正确。 在此情况下,函数应返回包含预期正文数据的正确响应。

    // Check that the response is an "OK" response
    Assert.IsAssignableFrom<OkObjectResult>(response.Result);
    
    // Check that the contents of the response are the expected contents
    var result = (OkObjectResult)response.Result;
    dynamic watchinfo = new { Manufacturer = "abc", CaseType = "Solid", Bezel = "Titanium", Dial = "Roman", CaseFinish = "Silver", Jewels = 15 };
    string watchInfo = $"Watch Details: {watchinfo.Manufacturer}, {watchinfo.CaseType}, {watchinfo.Bezel}, {watchinfo.Dial}, {watchinfo.CaseFinish}, {watchinfo.Jewels}";
    Assert.Equal(watchInfo, result.Value);
    

    完整的方法应如下所示。

    [Fact]
    public void TestWatchFunctionSuccess()
    {
        var queryStringValue = "abc";
        var request = new DefaultHttpRequest(new DefaultHttpContext())
        {
            Query = new QueryCollection
            (
                new System.Collections.Generic.Dictionary<string, StringValues>()
                {
                    { "model", queryStringValue }
                }
            )
        };
    
        var logger = NullLoggerFactory.Instance.CreateLogger("Null Logger");
    
        var response = WatchPortalFunction.WatchInfo.Run(request, logger);
        response.Wait();
    
        // Check that the response is an "OK" response
        Assert.IsAssignableFrom<OkObjectResult>(response.Result);
    
        // Check that the contents of the response are the expected contents
        var result = (OkObjectResult)response.Result;
        dynamic watchinfo = new { Manufacturer = "abc", CaseType = "Solid", Bezel = "Titanium", Dial = "Roman", CaseFinish = "Silver", Jewels = 15 };
        string watchInfo = $"Watch Details: {watchinfo.Manufacturer}, {watchinfo.CaseType}, {watchinfo.Bezel}, {watchinfo.Dial}, {watchinfo.CaseFinish}, {watchinfo.Jewels}";
        Assert.Equal(watchInfo, result.Value);
    }
    
  8. 再添加两个名为 TestWatchFunctionFailureNoQueryString 和 TestWatchFunctionFailureNoModel 的方法。 TestWatchFunctionFailureNoQueryString 验证如果没有为 WatchInfo 函数提供查询字符串,则会失败。 如果函数传递的查询字符串不包含模型参数,则 TestWatchFunctionFailureNoModel 检查相同的失败。

    [Fact]
    public void TestWatchFunctionFailureNoQueryString()
    {
        var request = new DefaultHttpRequest(new DefaultHttpContext());
        var logger = NullLoggerFactory.Instance.CreateLogger("Null Logger");
    
        var response = WatchPortalFunction.WatchInfo.Run(request, logger);
        response.Wait();
    
        // Check that the response is an "Bad" response
        Assert.IsAssignableFrom<BadRequestObjectResult>(response.Result);
    
        // Check that the contents of the response are the expected contents
        var result = (BadRequestObjectResult)response.Result;
        Assert.Equal("Please provide a watch model in the query string", result.Value);
    }
    
    [Fact]
    public void TestWatchFunctionFailureNoModel()
    {
        var queryStringValue = "abc";
        var request = new DefaultHttpRequest(new DefaultHttpContext())
        {
            Query = new QueryCollection
            (
                new System.Collections.Generic.Dictionary<string, StringValues>()
                {
                    { "not-model", queryStringValue }
                }
            )
        };
    
        var logger = NullLoggerFactory.Instance.CreateLogger("Null Logger");
    
        var response = WatchPortalFunction.WatchInfo.Run(request, logger);
        response.Wait();
    
        // Check that the response is an "Bad" response
        Assert.IsAssignableFrom<BadRequestObjectResult>(response.Result);
    
        // Check that the contents of the response are the expected contents
        var result = (BadRequestObjectResult)response.Result;
        Assert.Equal("Please provide a watch model in the query string", result.Value);
    }
    

运行测试

  1. 在顶部菜单栏的“测试”下,选择“运行所有测试”。

    Visual Studio 中“测试”菜单的屏幕截图。用户选择了“运行”->“所有测试”。

  2. 在“测试资源管理器”窗口中,应成功完成所有三项测试。

    “团队资源管理器”窗口的屏幕截图。所有三项测试都运行成功。

  3. 在“解决方案资源管理器”窗口中,在“WatchPortalFunction”项目下,双击“WatchInfo.cs”以在代码编辑器中显示文件。

  4. 找到以下代码。

    // Retrieve the model id from the query string
    string model = req.Query["model"];
    
  5. 更改设置 model 变量的语句,如下所示。 此更改模拟了开发者在代码中犯错。

    string model = req.Query["modelll"];
    
  6. 在顶部菜单栏的“测试”下,选择“运行所有测试”。 这一次,TestWatchFunctionSuccess 测试会失败。 发生此故障是因为 WatchInfo 函数未在查询字符串中找到名为 modelll 的参数,因此该函数返回错误响应

    “团队资源管理器”窗口的屏幕截图。TestWatchFunctionSuccess 测试失败。

在本单元中,你已了解了如何创建单元测试项目,以及如何为 Azure Functions 实现单元测试。