在 ASP.NET 核心中模拟 IPrincipal
Mocking IPrincipal in ASP.NET Core
我有一个 ASP.NET MVC Core 应用程序,我正在为其编写单元测试。其中一种操作方法使用用户名来实现某些功能:
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);
}
但这也引发了错误。
在文档中也找不到任何内容...
我希望实现抽象工厂模式。
创建一个工厂接口,专门提供用户名。
然后提供具体的 classes,一个提供 User.Identity.Name
,一个提供一些其他适用于您的测试的硬编码值。
然后您可以根据生产代码和测试代码使用适当的具体 class。也许希望将工厂作为参数传递,或者根据某些配置值切换到正确的工厂。
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());
在以前的版本中,您可以直接在控制器上设置 User
,这样可以进行一些非常简单的单元测试。
如果您查看 ControllerBase 的源代码,您会注意到 User
是从 HttpContext
中提取的。
/// <summary>
/// Gets the <see cref="ClaimsPrincipal"/> for user associated with the executing action.
/// </summary>
public ClaimsPrincipal User => HttpContext?.User;
控制器通过ControllerContext
访问HttpContext
/// <summary>
/// Gets the <see cref="Http.HttpContext"/> for the executing action.
/// </summary>
public HttpContext HttpContext => ControllerContext.HttpContext;
您会注意到这两个是只读属性。好消息是 ControllerContext
属性 允许设置它的值,这样你就可以进入了。
所以目标是到达那个物体。在 Core 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<ClaimsPrincipal>();
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
is accessed through the HttpContext
of the controller. The latter is stored within the 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
, make sure to pass an explicit authenticationType
to the constructor. This makes sure that IsAuthenticated
将正常工作(如果您在代码中使用它来确定用户是否已通过身份验证)。
也可以使用现有的 类,并仅在需要时进行模拟。
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();
我想直接点击我的控制器,然后像 AutoFac 一样使用 DI。为此,我首先注册 ContextController
.
var identity = new GenericIdentity("Test User");
var httpContext = new DefaultHttpContext()
{
User = new GenericPrincipal(identity, null)
};
var context = new ControllerContext { HttpContext = httpContext};
builder.RegisterInstance(context);
接下来我在注册控制器时启用 属性 注入。
builder.RegisterAssemblyTypes(assembly)
.Where(t => t.Name.EndsWith("Controller")).PropertiesAutowired();
然后 User.Identity.Name
被填充,在我的 Controller 上调用方法时我不需要做任何特殊的事情。
public async Task<ActionResult<IEnumerable<Employee>>> Get()
{
var requestedBy = User.Identity?.Name;
..................
我有一个 brownfield .net 4.8 项目需要转换为 .net 5.0,我想尽可能多地保留原始代码,包括 unit-/integration 测试。控制器的测试非常依赖上下文,所以我创建了这个扩展方法来启用设置令牌、声明和 headers:
public static void AddContextMock(
this ControllerBase controller,
IEnumerable<(string key, string value)> claims = null,
IEnumerable<(string key, string value)> tokens = null,
IEnumerable<(string key, string value)> headers = null)
{
HttpContext mockContext = new DefaultHttpContext();
if(claims != null)
{
mockContext.User = SetupClaims(claims);
}
if(tokens != null)
{
mockContext.RequestServices = SetupTokens(tokens);
}
if(headers != null)
{
SetupHeaders(mockContext, headers);
}
controller.ControllerContext = new ControllerContext()
{
HttpContext = mockContext
};
}
private static void SetupHeaders(HttpContext mockContext, IEnumerable<(string key, string value)> headers)
{
foreach(var header in headers)
{
mockContext.Request.Headers.Add(header.key, header.value);
}
}
private static ClaimsPrincipal SetupClaims(IEnumerable<(string key, string value)> claimValues)
{
var claims = claimValues.Select(c => new Claim(c.key, c.value));
return new ClaimsPrincipal(new ClaimsIdentity(claims, "mock"));
}
private static IServiceProvider SetupTokens(IEnumerable<(string key, string value)> tokenValues)
{
var mockServiceProvider = new Mock<IServiceProvider>();
var authenticationServiceMock = new Mock<IAuthenticationService>();
var authResult = AuthenticateResult.Success(
new AuthenticationTicket(new ClaimsPrincipal(), null));
var tokens = tokenValues.Select(t => new AuthenticationToken { Name = t.key, Value = t.value });
authResult.Properties.StoreTokens(tokens);
authenticationServiceMock
.Setup(x => x.AuthenticateAsync(It.IsAny<HttpContext>(), null))
.ReturnsAsync(authResult);
mockServiceProvider.Setup(_ => _.GetService(typeof(IAuthenticationService))).Returns(authenticationServiceMock.Object);
return mockServiceProvider.Object;
}
这使用 Moq,但可以适应其他模拟框架。身份验证类型被硬编码为“模拟”,因为我依赖默认身份验证,但这也可以提供。
它是这样使用的:
_controllerUnderTest.AddContextMock(
claims: new[]
{
(ClaimTypes.Name, "UserName"),
(ClaimTypes.MobilePhone, "1234"),
},
tokens: new[]
{
("access_token", "accessTokenValue")
},
headers: new[]
{
("header", "headerValue")
});
我有一个 ASP.NET MVC Core 应用程序,我正在为其编写单元测试。其中一种操作方法使用用户名来实现某些功能:
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);
}
但这也引发了错误。 在文档中也找不到任何内容...
我希望实现抽象工厂模式。
创建一个工厂接口,专门提供用户名。
然后提供具体的 classes,一个提供 User.Identity.Name
,一个提供一些其他适用于您的测试的硬编码值。
然后您可以根据生产代码和测试代码使用适当的具体 class。也许希望将工厂作为参数传递,或者根据某些配置值切换到正确的工厂。
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());
在以前的版本中,您可以直接在控制器上设置 User
,这样可以进行一些非常简单的单元测试。
如果您查看 ControllerBase 的源代码,您会注意到 User
是从 HttpContext
中提取的。
/// <summary>
/// Gets the <see cref="ClaimsPrincipal"/> for user associated with the executing action.
/// </summary>
public ClaimsPrincipal User => HttpContext?.User;
控制器通过ControllerContext
HttpContext
/// <summary>
/// Gets the <see cref="Http.HttpContext"/> for the executing action.
/// </summary>
public HttpContext HttpContext => ControllerContext.HttpContext;
您会注意到这两个是只读属性。好消息是 ControllerContext
属性 允许设置它的值,这样你就可以进入了。
所以目标是到达那个物体。在 Core 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<ClaimsPrincipal>();
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
is accessed through the HttpContext
of the controller. The latter is stored within the 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
, make sure to pass an explicit authenticationType
to the constructor. This makes sure that IsAuthenticated
将正常工作(如果您在代码中使用它来确定用户是否已通过身份验证)。
也可以使用现有的 类,并仅在需要时进行模拟。
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();
我想直接点击我的控制器,然后像 AutoFac 一样使用 DI。为此,我首先注册 ContextController
.
var identity = new GenericIdentity("Test User");
var httpContext = new DefaultHttpContext()
{
User = new GenericPrincipal(identity, null)
};
var context = new ControllerContext { HttpContext = httpContext};
builder.RegisterInstance(context);
接下来我在注册控制器时启用 属性 注入。
builder.RegisterAssemblyTypes(assembly)
.Where(t => t.Name.EndsWith("Controller")).PropertiesAutowired();
然后 User.Identity.Name
被填充,在我的 Controller 上调用方法时我不需要做任何特殊的事情。
public async Task<ActionResult<IEnumerable<Employee>>> Get()
{
var requestedBy = User.Identity?.Name;
..................
我有一个 brownfield .net 4.8 项目需要转换为 .net 5.0,我想尽可能多地保留原始代码,包括 unit-/integration 测试。控制器的测试非常依赖上下文,所以我创建了这个扩展方法来启用设置令牌、声明和 headers:
public static void AddContextMock(
this ControllerBase controller,
IEnumerable<(string key, string value)> claims = null,
IEnumerable<(string key, string value)> tokens = null,
IEnumerable<(string key, string value)> headers = null)
{
HttpContext mockContext = new DefaultHttpContext();
if(claims != null)
{
mockContext.User = SetupClaims(claims);
}
if(tokens != null)
{
mockContext.RequestServices = SetupTokens(tokens);
}
if(headers != null)
{
SetupHeaders(mockContext, headers);
}
controller.ControllerContext = new ControllerContext()
{
HttpContext = mockContext
};
}
private static void SetupHeaders(HttpContext mockContext, IEnumerable<(string key, string value)> headers)
{
foreach(var header in headers)
{
mockContext.Request.Headers.Add(header.key, header.value);
}
}
private static ClaimsPrincipal SetupClaims(IEnumerable<(string key, string value)> claimValues)
{
var claims = claimValues.Select(c => new Claim(c.key, c.value));
return new ClaimsPrincipal(new ClaimsIdentity(claims, "mock"));
}
private static IServiceProvider SetupTokens(IEnumerable<(string key, string value)> tokenValues)
{
var mockServiceProvider = new Mock<IServiceProvider>();
var authenticationServiceMock = new Mock<IAuthenticationService>();
var authResult = AuthenticateResult.Success(
new AuthenticationTicket(new ClaimsPrincipal(), null));
var tokens = tokenValues.Select(t => new AuthenticationToken { Name = t.key, Value = t.value });
authResult.Properties.StoreTokens(tokens);
authenticationServiceMock
.Setup(x => x.AuthenticateAsync(It.IsAny<HttpContext>(), null))
.ReturnsAsync(authResult);
mockServiceProvider.Setup(_ => _.GetService(typeof(IAuthenticationService))).Returns(authenticationServiceMock.Object);
return mockServiceProvider.Object;
}
这使用 Moq,但可以适应其他模拟框架。身份验证类型被硬编码为“模拟”,因为我依赖默认身份验证,但这也可以提供。
它是这样使用的:
_controllerUnderTest.AddContextMock(
claims: new[]
{
(ClaimTypes.Name, "UserName"),
(ClaimTypes.MobilePhone, "1234"),
},
tokens: new[]
{
("access_token", "accessTokenValue")
},
headers: new[]
{
("header", "headerValue")
});