我有一个ASP.NET MVC核心应用程序,我正在编写单元测试。其中一个操作方法使用用户名来实现某些功能:
SettingsViewModel svm = _context.MySettings(User.Identity.Name);
这显然在单元测试中失败了。我环顾四周,所有建议都是从.NET 4.5到模拟HttpContext。我相信有更好的方法可以做到这一点。我试图注入IPrincipal,但它引发了一个错误;我甚至试过这个(出于绝望,我想):
public IActionResult Index(IPrincipal principal = null) {
IPrincipal user = principal ?? User;
SettingsViewModel svm = _context.MySettings(user.Identity.Name);
return View(svm);
}
但这也引发了一个错误。无法在文档中找到任何内容......
控制器的User
is accessed通过HttpContext
of the controller。后者is stored在ControllerContext
内。
设置用户的最简单方法是为构造用户分配不同的HttpContext。我们可以将DefaultHttpContext
用于此目的,这样我们就不必模拟一切。然后我们在控制器上下文中使用该HttpContext并将其传递给控制器实例:
var user = new ClaimsPrincipal(new ClaimsIdentity(new Claim[]
{
new Claim(ClaimTypes.Name, "example name"),
new Claim(ClaimTypes.NameIdentifier, "1"),
new Claim("custom-claim", "example claim value"),
}, "mock"));
var controller = new SomeController(dependencies…);
controller.ControllerContext = new ControllerContext()
{
HttpContext = new DefaultHttpContext() { User = user }
};
在创建自己的ClaimsIdentity
时,请确保将明确的authenticationType
传递给构造函数。这可以确保IsAuthenticated
正常工作(如果您在代码中使用它来确定用户是否经过身份验证)。
在以前的版本中,您可以直接在控制器上设置User
,这样可以进行一些非常简单的单元测试。
如果你看一下ControllerBase的源代码,你会注意到User
是从HttpContext
中提取的。
/// <summary>
/// Gets or sets the <see cref="ClaimsPrincipal"/> for user associated with the executing action.
/// </summary>
public ClaimsPrincipal User
{
get
{
return HttpContext?.User;
}
}
并且控制器通过HttpContext
访问ControllerContext
/// <summary>
/// Gets the <see cref="Http.HttpContext"/> for the executing action.
/// </summary>
public HttpContext HttpContext
{
get
{
return ControllerContext.HttpContext;
}
}
您会注意到这两个是只读属性。好消息是,ControllerContext
属性允许设置它的值,这将是你的方式。
所以目标是获得该对象。在核心HttpContext
是抽象的,所以它更容易嘲笑。
假设一个控制器就像
public class MyController : Controller {
IMyContext _context;
public MyController(IMyContext context) {
_context = context;
}
public IActionResult Index() {
SettingsViewModel svm = _context.MySettings(User.Identity.Name);
return View(svm);
}
//...other code removed for brevity
}
使用Moq,测试看起来像这样
public void Given_User_Index_Should_Return_ViewResult_With_Model() {
//Arrange
var username = "FakeUserName";
var identity = new GenericIdentity(username, "");
var mockPrincipal = new Mock<IPrincipal>();
mockPrincipal.Setup(x => x.Identity).Returns(identity);
mockPrincipal.Setup(x => x.IsInRole(It.IsAny<string>())).Returns(true);
var mockHttpContext = new Mock<HttpContext>();
mockHttpContext.Setup(m => m.User).Returns(mockPrincipal.Object);
var model = new SettingsViewModel() {
//...other code removed for brevity
};
var mockContext = new Mock<IMyContext>();
mockContext.Setup(m => m.MySettings(username)).Returns(model);
var controller = new MyController(mockContext.Object) {
ControllerContext = new ControllerContext {
HttpContext = mockHttpContext.Object
}
};
//Act
var viewResult = controller.Index() as ViewResult;
//Assert
Assert.IsNotNull(viewResult);
Assert.IsNotNull(viewResult.Model);
Assert.AreEqual(model, viewResult.Model);
}
我希望实现一个抽象工厂模式。
为工厂创建专门用于提供用户名的界面。
然后提供具体的类,一个提供User.Identity.Name
,另一个提供适用于您的测试的其他硬编码值。
然后,您可以根据生产与测试代码使用适当的具体类。也许希望将工厂作为参数传递,或根据某些配置值切换到正确的工厂。
interface IUserNameFactory
{
string BuildUserName();
}
class ProductionFactory : IUserNameFactory
{
public BuildUserName() { return User.Identity.Name; }
}
class MockFactory : IUserNameFactory
{
public BuildUserName() { return "James"; }
}
IUserNameFactory factory;
if(inProductionMode)
{
factory = new ProductionFactory();
}
else
{
factory = new MockFactory();
}
SettingsViewModel svm = _context.MySettings(factory.BuildUserName());
还可以使用现有的类,并仅在需要时进行模拟。
var user = new Mock<ClaimsPrincipal>();
_controller.ControllerContext = new ControllerContext
{
HttpContext = new DefaultHttpContext
{
User = user.Object
}
};
就我而言,我需要使用Request.HttpContext.User.Identity.IsAuthenticated
,Request.HttpContext.User.Identity.Name
和一些坐在控制器外部的业务逻辑。我能够结合使用Nkosi,Calin和Poke的答案:
var identity = new Mock<IIdentity>();
identity.SetupGet(i => i.IsAuthenticated).Returns(true);
identity.SetupGet(i => i.Name).Returns("FakeUserName");
var mockPrincipal = new Mock<ClaimsPrincipal>();
mockPrincipal.Setup(x => x.Identity).Returns(identity.Object);
var mockAuthHandler = new Mock<ICustomAuthorizationHandler>();
mockAuthHandler.Setup(x => x.CustomAuth(It.IsAny<ClaimsPrincipal>(), ...)).Returns(true).Verifiable();
var controller = new MyController(...);
var mockHttpContext = new Mock<HttpContext>();
mockHttpContext.Setup(m => m.User).Returns(mockPrincipal.Object);
controller.ControllerContext = new ControllerContext();
controller.ControllerContext.HttpContext = new DefaultHttpContext()
{
User = mockPrincipal.Object
};
var result = controller.Get() as OkObjectResult;
//Assert results
mockAuthHandler.Verify();
您可以使用IHttpContextAccessor在Net Core中模拟HttpContext,如下所示:
public class UserRepository : IUserRepository
{
private readonly IHttpContextAccessor _httpContextAccessor;
public UserRepository(IHttpContextAccessor httpContextAccessor)
{
_httpContextAccessor = httpContextAccessor;
}
public void LogCurrentUser()
{
var username = _httpContextAccessor.HttpContext.User.Identity.Name;
service.LogAccessRequest(username);
}
}
这取自本页:https://docs.microsoft.com/en-us/aspnet/core/fundamentals/http-context?view=aspnetcore-2.2