在 C# 单元测试中使用 HttpContext.GetTokenAsync

Noa*_*pas 4 c# unit-testing moq xunit httpcontext

我正在尝试为我的控制器类编写单元测试,它使用以下命令检索令牌:

string token = await HttpContext.GetTokenAsync("access_token");
Run Code Online (Sandbox Code Playgroud)

因此,我用以下代码模拟了 HttpContext:

public static HttpContext MakeFakeContext()
{
    var serviceProvider = new Mock<IServiceProvider>();
    var authservice = new Mock<IAuthenticationService>();

    authservice.Setup(_ => _.GetTokenAsync(It.IsAny<HttpContext>(), It.IsAny<string>())).Returns(Task.FromResult("token"));
    serviceProvider.Setup(_ => _.GetService(typeof(IAuthenticationService))).Returns(authservice);

    return new DefaultHttpContext
    {
        RequestServices = serviceProvider.Object
    };
}
Run Code Online (Sandbox Code Playgroud)

我正在设置模拟上下文:

var mockcontext = MakeFakeContext();

unitUnderTest.ControllerContext = new ControllerContext
{
    HttpContext = mockcontext
};
Run Code Online (Sandbox Code Playgroud)

现在,当我运行单元测试时,出现以下错误:

System.NotSupportedException:不支持的表达式:_ => _.GetTokenAsync(It.IsAny(), It.IsAny()) 扩展方法(此处:AuthenticationTokenExtensions.GetTokenAsync)不能用于设置/验证表达式。

在我的研究过程中,我偶然发现了一些解决方案,您可以在其中模拟引擎盖下不属于扩展的特定部分。这些是其中一些:Moq IServiceProvider / IServiceScope如何对 HttpContext.SignInAsync() 进行单元测试?. 第二个显示了类似的问题,在我尝试后似乎有效。但由于某种原因,它不适用于 GetTokenAsync 方法。

大家有什么提示吗?

Sta*_*cey 7

以 Zer0 的回答为基础,这里是一个使用 Moq 的示例:

    private void MockHttpContextGetToken(
        Mock<IHttpContextAccessor> httpContextAccessorMock,
        string tokenName, string tokenValue, string scheme = null)
    {
        var authenticationServiceMock = new Mock<IAuthenticationService>();
        httpContextAccessorMock
            .Setup(x => x.HttpContext.RequestServices.GetService(typeof(IAuthenticationService)))
            .Returns(authenticationServiceMock.Object);

        var authResult = AuthenticateResult.Success(
            new AuthenticationTicket(new ClaimsPrincipal(), scheme));

        authResult.Properties.StoreTokens(new[]
        {
            new AuthenticationToken { Name = tokenName, Value = tokenValue }
        });

        authenticationServiceMock
            .Setup(x => x.AuthenticateAsync(httpContextAccessorMock.Object.HttpContext, scheme))
            .ReturnsAsync(authResult);
    }
Run Code Online (Sandbox Code Playgroud)


小智 7

这对我有用

controller.ControllerContext = new ControllerContext();
var serviceProvider = new Mock<IServiceProvider>();
var authenticationServiceMock = new Mock<IAuthenticationService>();
var authResult = AuthenticateResult.Success(
    new AuthenticationTicket(new ClaimsPrincipal(), null));

authResult.Properties.StoreTokens(new[]
{
    new AuthenticationToken { Name = "access_token", Value = "accessTokenValue" }
});

authenticationServiceMock
    .Setup(x => x.AuthenticateAsync(It.IsAny<HttpContext>(), null))
    .ReturnsAsync(authResult);

serviceProvider.Setup(_ => _.GetService(typeof(IAuthenticationService))).Returns(authenticationServiceMock.Object);

controller.ControllerContext.HttpContext = new DefaultHttpContext
{
    User = user,
    RequestServices = serviceProvider.Object
};
Run Code Online (Sandbox Code Playgroud)


Zer*_*er0 5

这是该扩展方法的源代码:

public static Task<string> GetTokenAsync(this HttpContext context, string scheme, 
string tokenName) =>
    context.RequestServices.GetRequiredService<IAuthenticationService> 
    ().GetTokenAsync(context, scheme, tokenName);
Run Code Online (Sandbox Code Playgroud)

依次调用此扩展方法:

public static async Task<string> GetTokenAsync(this IAuthenticationService auth, HttpContext context, string scheme, string tokenName)
{
    if (auth == null)
    {
        throw new ArgumentNullException(nameof(auth));
    }
    if (tokenName == null)
    {
        throw new ArgumentNullException(nameof(tokenName));
    }
    var result = await auth.AuthenticateAsync(context, scheme);
    return result?.Properties?.GetTokenValue(tokenName);
}
Run Code Online (Sandbox Code Playgroud)

最终结果是调用,AuthenticateAsync如行中所示var result = await auth.AuthenticateAsync(context, scheme);

既然你不能修改扩展方法,也许你可以编写自己的模拟方法?

我不确定在对具有扩展方法的对象进行模拟时的最佳实践是什么,所以也许有人可以扩展这个答案。

值得注意的AuthenticateAsync不是一个扩展方法,你可以在这里找到代码。

正如@Nkosi 所提到的:

模拟IServiceProviderIAuthenticationService

恕我直言,看到真正的代码总是有用的,这样你就可以识别和理解它在幕后做了什么,并完全模拟出所有需要的部分,所以我会留下上面的内容。