如何对 HttpContext.SignInAsync() 进行单元测试?
How to unit test HttpContext.SignInAsync()?
我 运行 遇到一些单元测试问题。
DefaultHttpContext.RequestServices
是 null
- 我试图创建
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);
我 运行 遇到一些单元测试问题。
DefaultHttpContext.RequestServices
是 null- 我试图创建
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);