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

Using HttpContext.GetTokenAsync in C# Unit Tests

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

string token = await HttpContext.GetTokenAsync("access_token");

因此我使用以下代码模拟了 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
    };
}

我正在设置模拟上下文:

var mockcontext = MakeFakeContext();

unitUnderTest.ControllerContext = new ControllerContext
{
    HttpContext = mockcontext
};

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

System.NotSupportedException : Unsupported expression: _ => _.GetTokenAsync(It.IsAny(), It.IsAny()) Extension methods (here: AuthenticationTokenExtensions.GetTokenAsync) may not be used in setup / verification expressions.

在我的研究过程中,我偶然发现了一些解决方案,您可以在其中模拟幕后涉及的特定部分,这些部分不属于扩展。这些是其中的一些:, 。第二个显示了类似的问题,我尝试后似乎有效。但由于某种原因,它不适用于 GetTokenAsync 方法。

你们有什么提示吗?

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

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

依次调用这个扩展方法:

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);
}

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

行所示

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

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

值得注意的是AuthenticateAsync不是扩展方法,您可以找到代码here.

如@Nkosi 所述:

模拟 IServiceProviderIAuthenticationService.

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

这里以 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);
    }

这对我有用

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
};