我正在开发一个 CustomClaimsProvider,尽管就这个问题而言,重要的是它是一个带有 HTTP 触发器的 Azure 函数。此处的代码基于入门页面的编辑函数部分中的示例代码。
问题是相同的代码在不同的实现中给出不同的响应,我想要一个单元测试来验证响应是否正确。
重现步骤:
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();
}
{
"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"
]
}
}
]
}
}
但是,如果我们重复这些步骤,并且只更改以下内容...
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 进行单元测试就可以发现问题。
我没有参与过任何需要单元测试来检查序列化格式的项目,所以我不确定这是一个需要解决的有效问题。 (也许集成测试会更好?)但是您请求帮助使其可进行单元测试,所以我会给出一些想法。
我会建议三个选项。两者都基于将逻辑从 Azure Function 移至单独的 Service 类的原则。单元测试最好远离端点技术堆栈来完成,因此它们只作用于域逻辑。
从 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 序列化器。您可以在单元测试中添加一些辅助方法,这样您就不会一直重复该代码。
我在这里省略了这个细节,因为我假设您已经知道如何做到这一点。如果没有,请告诉我,我也会添加。
这是上面的变体。让 Service 方法返回实际的
string
作为其输出,而不是 ResponseContent
。我不太喜欢这个,因为它将两种不同的东西混合到一个服务中 - 实际上混合到应用程序的一层中:
所以我更喜欢选项 1。
感觉您应该重新构建应用程序,以便用户身份验证代码不在主函数或甚至主服务层中。完全抽象您的身份验证逻辑可能会更好;也许将其作为依赖项引入并使用 Azure Function 中的一些中间件类来完成此操作。
基本上,身份验证与端点或应用程序逻辑的关注点略有不同,因此它也可能是分开的。但这超出了您的问题范围。