如何对 HttpContext.SignInAsync() 进行单元测试?

How to unit test HttpContext.SignInAsync()?

SignInAsync() Source Code

我 运行 遇到一些单元测试问题。

  1. DefaultHttpContext.RequestServicesnull
  2. 我试图创建 AuthenticationService 对象,但我不知道要传递什么参数

我该怎么办?如何进行单元测试 HttpContext.SignInAsync()?

正在测试的方法

public async Task<IActionResult> Login(LoginViewModel vm, [FromQuery]string returnUrl)
{
    if (ModelState.IsValid)
    {
        var user = await context.Users.FirstOrDefaultAsync(u => u.UserName == vm.UserName && u.Password == vm.Password);
        if (user != null)
        {
            var claims = new List<Claim>
            {
                new Claim(ClaimTypes.Name, user.UserName)
            };
            var identity = new ClaimsIdentity(claims, "HappyDog");

            // here
            await HttpContext.SignInAsync(new ClaimsPrincipal(identity));
            return Redirect(returnUrl ?? Url.Action("Index", "Goods"));
        }
    }
    return View(vm);
}

到目前为止我已经尝试了什么。

[TestMethod]
public async Task LoginTest()
{
    using (var context = new HappyDogContext(_happyDogOptions))
    {
        await context.Users.AddAsync(new User { Id = 1, UserName = "test", Password = "password", FacePicture = "FacePicture" });
        await context.SaveChangesAsync();

        var controller = new UserController(svc, null)
        {
            ControllerContext = new ControllerContext
            {
                HttpContext = new DefaultHttpContext
                {
                    // How mock RequestServices?
                    // RequestServices = new AuthenticationService()?
                }
            }
        };
        var vm = new LoginViewModel { UserName = "test", Password = "password" };
        var result = await controller.Login(vm, null) as RedirectResult;
        Assert.AreEqual("/Goods", result.Url);
    }
}

HttpContext.SignInAsync是使用RequestServices的扩展方法,也就是IServiceProvider。那是你必须嘲笑的。

context.RequestServices
    .GetRequiredService<IAuthenticationService>()
    .SignInAsync(context, scheme, principal, properties);

您可以通过创建从使用的接口派生的 类 手动创建 fake/mock,或者使用像 Moq

这样的模拟框架
//...code removed for brevity

var authServiceMock = new Mock<IAuthenticationService>();
authServiceMock
    .Setup(_ => _.SignInAsync(It.IsAny<HttpContext>(), It.IsAny<string>(), It.IsAny<ClaimsPrincipal>(), It.IsAny<AuthenticationProperties>()))
    .Returns(Task.FromResult((object)null));

var serviceProviderMock = new Mock<IServiceProvider>();
serviceProviderMock
    .Setup(_ => _.GetService(typeof(IAuthenticationService)))
    .Returns(authServiceMock.Object);

var controller = new UserController(svc, null) {
    ControllerContext = new ControllerContext {
        HttpContext = new DefaultHttpContext {
            // How mock RequestServices?
            RequestServices = serviceProviderMock.Object
        }
    }
};

//...code removed for brevity

您可以在 Quick start

上阅读有关如何使用 Moq 的信息

您可以像模拟其他依赖项一样轻松地模拟 HttpContext,但是如果存在不会导致不良行为的默认实现,那么使用它可以使事情的安排变得更加简单

例如,实际的 IServiceProvider 可以通过 ServiceCollection

构建一个来使用
//...code removed for brevity

var authServiceMock = new Mock<IAuthenticationService>();
authServiceMock
    .Setup(_ => _.SignInAsync(It.IsAny<HttpContext>(), It.IsAny<string>(), It.IsAny<ClaimsPrincipal>(), It.IsAny<AuthenticationProperties>()))
    .Returns(Task.FromResult((object)null));

var services = new ServiceCollection();
services.AddSingleton<IAuthenticationService>(authServiceMock.Object);

var controller = new UserController(svc, null) {
    ControllerContext = new ControllerContext {
        HttpContext = new DefaultHttpContext {
            // How mock RequestServices?
            RequestServices = services.BuildServiceProvider();
        }
    }
};

//...code removed for brevity

这样,如果有其他依赖项,它们可以被模拟并注册到服务集合中,以便可以根据需要解决它们。

如果你们正在寻找 NSubstitue 示例(Asp.net 核心)。

    IAuthenticationService authenticationService = Substitute.For<IAuthenticationService>();

        authenticationService
            .SignInAsync(Arg.Any<HttpContext>(), Arg.Any<string>(), Arg.Any<ClaimsPrincipal>(),
                Arg.Any<AuthenticationProperties>()).Returns(Task.FromResult((object) null));

        var serviceProvider = Substitute.For<IServiceProvider>();
        var authSchemaProvider = Substitute.For<IAuthenticationSchemeProvider>();
        var systemClock = Substitute.For<ISystemClock>();

        authSchemaProvider.GetDefaultAuthenticateSchemeAsync().Returns(Task.FromResult
        (new AuthenticationScheme("idp", "idp", 
            typeof(IAuthenticationHandler))));

        serviceProvider.GetService(typeof(IAuthenticationService)).Returns(authenticationService);
        serviceProvider.GetService(typeof(ISystemClock)).Returns(systemClock);
        serviceProvider.GetService(typeof(IAuthenticationSchemeProvider)).Returns(authSchemaProvider);

        context.RequestServices.Returns(serviceProvider);


        // Your act goes here

        // Your assert goes here

这在 .NET Core 2.2 中对我不起作用 - 它仍然需要另一个接口:ISystemClock。所以我干脆决定采用另一种方法,即包装整个东西,像这样:

using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Mvc;

namespace Utilities.HttpContext
{
    public interface IHttpContextWrapper
    {
        Task SignInAsync(Controller controller, string subject, string name, AuthenticationProperties props);
    }
}

...然后我有一个用于正常使用和测试的实现。

using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Http;

namespace Utilities.HttpContext
{
    public class DefaultHttpContextWrapper : IHttpContextWrapper
    {
        public async Task SignInAsync(Controller controller, string subject, string name, AuthenticationProperties props)
        {
            await controller.HttpContext.SignInAsync(subject, name, props);
        }
    }
}

...以及伪造的实现:

using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Mvc;

namespace Utilities.HttpContext
{
    public class FakeHttpContextWrapper : IHttpContextWrapper
    {
        public Task SignInAsync(Controller controller, string subject, string name, AuthenticationProperties props)
        {
            return Task.CompletedTask;
        }
    }
}

然后,我使用 .NET Core 的本机 DI 容器(在 Startup.cs 中)将所需的实现作为接口注入到控制器的构造函数中。

services.AddScoped<IHttpContextWrapper, DefaultHttpContextWrapper>();

最后,调用看起来像这样(传入我的控制器):

await _httpContextWrapper.SignInAsync(this, user.SubjectId, user.Username, props);