如何在单元测试ASP.NET Core控制器时正确模拟IAuthenticationHandler

5
我正在尝试对我的AccountController中的Login方法进行单元测试,参考thisMusiStore示例。请注意,保留HTML标签。
// POST: /Account/Login
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Login(LoginArgumentsModel model)
{
   if (!ModelState.IsValid)
   {
      return BadRequest();
   }
   var result = await _signInManager.PasswordSignInAsync(model.UserName, model.Password, model.RememberMe, lockoutOnFailure: false);
   if (result.Succeeded)
   {
      return Ok();
   }
return StatusCode(422); // Unprocessable Entity
}

为此,我需要同时使用UserManagerSignInManager,这最终迫使我编写一个替代IAuthenticationHandler以在HttpAuthenticationFeature中使用。最终测试结果如下:
public class AccountControllerTestsFixture : IDisposable
{
    public IServiceProvider BuildServiceProvider(IAuthenticationHandler handler)
    {
        var efServiceProvider = new ServiceCollection().AddEntityFrameworkInMemoryDatabase().BuildServiceProvider();

        var services = new ServiceCollection();
        services.AddOptions();
        services.AddDbContext<ApplicationDbContext>(b => b.UseInMemoryDatabase().UseInternalServiceProvider(efServiceProvider));

        services.AddIdentity<ApplicationUser, IdentityRole>(o =>
        {
            o.Password.RequireDigit = false;
            o.Password.RequireLowercase = false;
            o.Password.RequireUppercase = false;
            o.Password.RequireNonAlphanumeric = false;
            o.Password.RequiredLength = 3;
        }).AddEntityFrameworkStores<ApplicationDbContext>();

            // IHttpContextAccessor is required for SignInManager, and UserManager
        var context = new DefaultHttpContext();

        context.Features.Set<IHttpAuthenticationFeature>(new HttpAuthenticationFeature { Handler = handler });

        services.AddSingleton<IHttpContextAccessor>(new HttpContextAccessor()
        {
            HttpContext = context
        });

        return services.BuildServiceProvider();
    }

    public Mock<IAuthenticationHandler> MockSignInHandler()
    {
        var handler = new Mock<IAuthenticationHandler>();
        handler.Setup(o => o.AuthenticateAsync(It.IsAny<AuthenticateContext>())).Returns<AuthenticateContext>(c =>
        {
            c.NotAuthenticated();
            return Task.FromResult(0);
        });
        handler.Setup(o => o.SignInAsync(It.IsAny<SignInContext>())).Returns<SignInContext>(c =>
        {
            c.Accept();
            return Task.FromResult(0);
        });

        return handler;
    }
    public void Dispose(){}
}

和这个:

public class AccountControllerTests : IClassFixture<AccountControllerTestsFixture>
{
    private AccountControllerTestsFixture _fixture;

    public AccountControllerTests(AccountControllerTestsFixture fixture)
    {
        _fixture = fixture;
    }

    [Fact]
    public async Task Login_When_Present_Provider_Version()
    {
        // Arrange
        var mockedHandler = _fixture.MockSignInHandler();
        IServiceProvider serviceProvider = _fixture.BuildServiceProvider(mockedHandler.Object);

        var userName = "Flattershy";
        var userPassword = "Angel";
        var claims = new List<Claim> { new Claim(ClaimTypes.NameIdentifier, userName) };

        var userManager = serviceProvider.GetRequiredService<UserManager<ApplicationUser>>();
        var userManagerResult = await userManager.CreateAsync(new ApplicationUser() { Id = userName, UserName = userName, TwoFactorEnabled = false }, userPassword);

        Assert.True(userManagerResult.Succeeded);

        var signInManager = serviceProvider.GetRequiredService<SignInManager<ApplicationUser>>();

        AccountController controller = new AccountController(userManager, signInManager);

        // Act
        var model = new LoginArgumentsModel { UserName = userName, Password = userPassword };
        var result = await controller.Login(model) as Microsoft.AspNetCore.Mvc.StatusCodeResult;

        // Assert
        Assert.Equal((int)System.Net.HttpStatusCode.OK, result.StatusCode);
    }

}

我觉得既要使用serviceProvider,又不想模拟userManager和signInManager,多次嘲笑IAuthenticationHandler和为每个测试创建实现IAuthenticationHandler的多个类看起来有些过于复杂。虽然这种方式编写的测试似乎有效,但我想知道是否有任何不太复杂的方法可以使用CookieAuthenticationHandler或任何其他表现出与应用程序app.UseIdentity()相同行为的东西。

1个回答

0

你能否模拟SignInManager,将其添加到服务集合中,并设置一次调用_signInManager.PasswordSignInAsync(model.UserName, model.Password, model.RememberMe, lockoutOnFailure: false)以返回你想要测试控制器的结果?


我可以并且已经这样做了几次,但是这种方式对我来说感觉不太对,而且我认为这样的调用不会影响模拟的 HttpContext,因此一个更复杂的控制器可能无法正常运行。另外,我希望它尽可能接近原始行为,而我并不擅长模拟对象。 - FluffyOwl

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接