如何测试Azure函数ObjectResult的JSON序列化

问题描述 投票:0回答:1

我正在开发一个 CustomClaimsProvider,尽管就这个问题而言,重要的是它是一个带有 HTTP 触发器的 Azure 函数。此处的代码基于入门页面的编辑函数部分中的示例代码。

问题是相同的代码在不同的实现中给出不同的响应,我想要一个单元测试来验证响应是否正确。

重现步骤:

  1. 在 Visual Studio 中,创建一个新项目,使用“Azure Functions”(您需要安装 Azure 工作负载才能执行此操作)选择“.NET 6.0(长期支持)”(这很关键,它给出了“in- process”实现),但使用其他默认选项
  2. 将 Function1.cs 的内容替换为以下内容
using System.IO;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.AspNetCore.Http;
using Newtonsoft.Json;
using System.Collections.Generic;

namespace FunctionApp6;

public class Function1
{
    [FunctionName("CustomClaimsProvider")]
    public async Task<IActionResult> Run([HttpTrigger(AuthorizationLevel.Function, "get", "post")] HttpRequest request)
    {
        string requestBody = await new StreamReader(request.Body).ReadToEndAsync();

        // Claims to return to Azure AD
        ResponseContent r = new ResponseContent();
        r.data.actions[0].claims.ApiVersion = "1.0.0";
        r.data.actions[0].claims.DateOfBirth = "2000-01-01";
        r.data.actions[0].claims.CustomRoles.Add("Writer");
        r.data.actions[0].claims.CustomRoles.Add("Editor");
        return new OkObjectResult(r);
    }
}

public class ResponseContent
{
    public Data data { get; set; } = new();
}

public class Data
{
    public Data()
    {
        actions = new List<Action>();
        actions.Add(new Action());
    }

    [JsonProperty("@odata.type")]
    public string odatatype { get; set; } = "microsoft.graph.onTokenIssuanceStartResponseData";

    public List<Action> actions { get; set; }
}

public class Action
{
    [JsonProperty("@odata.type")]
    public string odatatype { get; set; } = "microsoft.graph.tokenIssuanceStart.provideClaimsForToken";

    public Claims claims { get; set; } = new();
}

public class Claims
{
    [JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
    public string AnotherValue { get; set; }

    [JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
    public string DateOfBirth { get; set; }

    public string ApiVersion { get; set; }

    public List<string> CustomRoles { get; set; } = new();
}
  1. 将该项目设置为启动项目并运行解决方案。当它运行时,它会给出一个 GET、POST URL...将该 URL 复制到浏览器中以查看返回的 JSON。输出 JSON 如下所示,这是正确的:
{
    "data": {
        "@odata.type": "microsoft.graph.onTokenIssuanceStartResponseData",
        "actions": [
            {
                "@odata.type": "microsoft.graph.tokenIssuanceStart.provideClaimsForToken",
                "claims": {
                    "dateOfBirth": "2000-01-01",
                    "apiVersion": "1.0.0",
                    "customRoles": [
                        "Writer",
                        "Editor"
                    ]
                }
            }
        ]
    }
}

但是,如果我们重复这些步骤,并且只更改以下内容...

  • 这次选择“.NET 6.0隔离(长期支持)”
  • 使用上面相同的代码,但将之前的所有“using”替换为:
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.Functions.Worker;
using Newtonsoft.Json;
  • 将函数上的属性名称从
    Function
    更改为
    FunctionName
  • string
    中的 3 个
    Claims
    属性的类型更改为
    string?

仅此而已。 现在将此项目设置为启动项目并运行它。现在的输出看起来像这样

{
    "data": {
        "odatatype": "microsoft.graph.onTokenIssuanceStartResponseData",
        "actions": [
            {
                "odatatype": "microsoft.graph.tokenIssuanceStart.provideClaimsForToken",
                "claims": {
                    "anotherValue": null,
                    "dateOfBirth": "2000-01-01",
                    "apiVersion": "1.0.0",
                    "customRoles": [
                        "Writer",
                        "Editor"
                    ]
                }
            }
        ]
    }
}

尽管实现相同,但输出不同。它已使用“System.Text.Json”而不是“Newtonsoft.Json”进行序列化。从代码中看不出这个决定是如何做出的;但这种差异是至关重要的,因为它破坏了实现。

我尝试开始编写 NUnit 测试,但意识到我不知道 IActionResult 如何成为响应正文中的 JSON:

        [Test]
        public void Verify_json_data_is_serialized_correctly()
        {
            // Arrange.
            ResponseContent r = new ResponseContent();

            // Act.
            var actual = new OkObjectResult(r);

            // Assert.
            Assert.That(actual, Is.EqualTo("doesn't matter"));
        }

如何对 JSON 响应是否正确序列化进行单元测试? (即测试方法返回的内容而不需要运行该函数)

附注我知道我可以添加 System.Text.Json 属性以使其正常工作,例如

[JsonPropertyName("@odata.type")]
...这不是重点。关键是浪费了来试图追踪为什么新的自定义声明提供程序无法工作,即使它基于已经在其他地方工作的代码。对 JSON 进行单元测试就可以发现问题。

unit-testing azure-functions json-serialization
1个回答
0
投票

注意事项

我没有参与过任何需要单元测试来检查序列化格式的项目,所以我不确定这是一个需要解决的有效问题。 (也许集成测试会更好?)但是您请求帮助使其可进行单元测试,所以我会给出一些想法。

建议

我会建议三个选项。两者都基于将逻辑从 Azure Function 移至单独的 Service 类的原则。单元测试最好远离端点技术堆栈来完成,因此它们只作用于域逻辑。

选项 1 - 在单元测试中进行序列化

从 Azure Function 中删除逻辑,并将其放入 Service 类中(可能在单独的项目中)。您没有指定用户的请求正文需要哪些信息,所以我无法猜测。

namespace MyServiceLayer;

public class MyCustomClaimsProvider
{
    public ResponseContent CreateClaimForUser(/*TODO: some user data*/)
    {
        ResponseContent r = new ResponseContent();
        r.data.actions[0].claims.ApiVersion = "1.0.0";
        r.data.actions[0].claims.DateOfBirth = "2000-01-01";
        r.data.actions[0].claims.CustomRoles.Add("Writer");
        r.data.actions[0].claims.CustomRoles.Add("Editor");

        return r;
    }
}

您的所有

ResponseContent
类也将与上述类一起进入服务层项目。

然后,您的 Azure 函数会注入此服务:

public class Function1
{
    private readonly MyCustomClaimsProvider _claimsProvider;

    public Function1(MyCustomClaimsProvider claimsProvider)
    {
        _claimsProvider = claimsProvider; // NB remember to set this up in DI.
    }

    [FunctionName("CustomClaimsProvider")]
    public async Task<IActionResult> Run([HttpTrigger(AuthorizationLevel.Function, "get", "post")] HttpRequest request)
    {
        string requestBody = await new StreamReader(request.Body).ReadToEndAsync();

        // Claims to return to Azure AD
        ResponseContent r = _claimsProvider.CreateClaimForUser(/*TODO: some user data*/);

        return new OkObjectResult(r);
    }
}

然后,编写单元测试以测试 MyCustomClaimsProvider。在单元测试中,确保使用与 Azure Functions SDK 默认值相同的 Json 序列化器。您可以在单元测试中添加一些辅助方法,这样您就不会一直重复该代码。

我在这里省略了这个细节,因为我假设您已经知道如何做到这一点。如果没有,请告诉我,我也会添加。

选项 2 - 让服务类返回一个字符串

这是上面的变体。让 Service 方法返回实际的

string
作为其输出,而不是
ResponseContent
。我不太喜欢这个,因为它将两种不同的东西混合到一个服务中 - 实际上混合到应用程序的一层中:

  • 领域逻辑(即服务)
  • 端点逻辑(即序列化)

所以我更喜欢选项 1。

选项3(有点旁白)

感觉您应该重新构建应用程序,以便用户身份验证代码不在主函数或甚至主服务层中。完全抽象您的身份验证逻辑可能会更好;也许将其作为依赖项引入并使用 Azure Function 中的一些中间件类来完成此操作。

基本上,身份验证与端点或应用程序逻辑的关注点略有不同,因此它也可能是分开的。但这超出了您的问题范围。

© www.soinside.com 2019 - 2024. All rights reserved.