如何模拟 User.Identity.GetUserId()?

36

我正在尝试对包含以下代码行的代码进行单元测试:

UserLoginInfo userIdentity = UserManager.GetLogins(User.Identity.GetUserId()).FirstOrDefault();

我只卡在了一个地方,因为我无法理解:

User.Identity.GetUserId()

为了返回一个值,我一直在尝试在我的控制器设置中使用以下代码:

var mock = new Mock<ControllerContext>();
mock.Setup(p => p.HttpContext.User.Identity.GetUserId()).Returns("string");

但是它会出现“NotSupportedException was unhandled by user code”的错误。我也尝试了以下方法:

ControllerContext controllerContext = new ControllerContext();

string username = "username";
string userid = Guid.NewGuid().ToString("N"); //could be a constant

List<Claim> claims = new List<Claim>{
    new Claim("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name", username), 
    new Claim("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier", userid)
};
var genericIdentity = new GenericIdentity("Andrew");
genericIdentity.AddClaims(claims);
var genericPrincipal = new GenericPrincipal(genericIdentity, new string[] { });
controllerContext.HttpContext.User = genericPrincipal;
根据我在stackoverflow上找到的一些代码,但是这还是会出现错误"NotSupportedException was unhandled by user code"。如何继续操作需要帮助。谢谢。

2
实现这一点的标准方法是创建一个新服务,即 IIdentity,将其抽象出来,使其与 GetUserId 扩展方法分离,并将其设置为调用您的 UserManager 的代码的依赖项。 - Sebastian Piu
刚刚意识到我的原始答案只适用于派生的ApiControllers。我更新了我的答案,提供了一个适用于派生控制器的选项,以及一个适用于两者的第二种方法。 - joelmdev
3个回答

42

你无法直接模拟扩展方法,因此你最好一直钻下去,直到找到扩展方法所依赖的属性和方法是可模拟的。

在这种情况下,IIdentity.GetuUserId() 是一个扩展方法。我可以发帖子来展示它,但该库目前不是开源的,因此你需要自己查看 GetUserId() 依赖于 ClaimsIdentity.FindFirstValue()。实际上,这也是一个扩展方法,但它依赖于标记为virtualClaimsIdentity.FindFirst()。现在我们有了缝隙,因此我们可以执行以下操作:

var claim = new Claim("test", "IdOfYourChoosing");
var mockIdentity =
    Mock.Of<ClaimsIdentity>(ci => ci.FindFirst(It.IsAny<string>()) == claim);
var controller = new MyController()
{
    User = Mock.Of<IPrincipal>(ip => ip.Identity == mockIdentity)
};

controller.User.Identity.GetUserId(); //returns "IdOfYourChoosing"

更新: 我刚意识到我上面发布的解决方案仅适用于派生的ApiControllers(例如,Web API)。MVC Controller类没有可设置的User属性。不过,通过ControllerControllerContext进入以实现所需效果也很容易:

var claim = new Claim("test", "IdOfYourChoosing");
var mockIdentity =
    Mock.Of<ClaimsIdentity>(ci => ci.FindFirst(It.IsAny<string>()) == claim);
var mockContext = Mock.Of<ControllerContext>(cc => cc.HttpContext.User == mockIdentity);
var controller = new MyController()
{
    ControllerContext = mockContext
};

但是,如果您使用 User 对象的唯一目的是获取用户的 Id,则有另一种方法可以适用于任何类型的控制器,并且需要更少的代码:

然而,如果你只是需要获取用户id,那么有一种更简单的方法适用于任何类型的控制器,并且需要更少的代码:
public class MyController: Controller //or ApiController
{
    public Func<string> GetUserId; //For testing

    public MyController()
    {
        GetUserId = () => User.Identity.GetUserId();
    }
    //controller actions
}

现在,当您需要用户的 ID 时,不再调用 User.Identity.GetUserId(),而是直接调用 GetUserId()。 当您需要在测试中模拟用户 ID 时,只需这样做:

controller = new MyController()
{
    GetUserId = () => "IdOfYourChoosing"
};

这两种方法都有优缺点。第一种方法更彻底、更灵活,利用了两种控制器类型中已有的接口,但需要编写更多的代码,读起来也不够流畅。第二种方法更简洁,表达能力我认为更强,但需要额外的代码维护,并且如果您不使用 Func 调用获取用户的 Id,则测试将无法正常工作。请选择在您的情况下更适合的方法。


1
配置调用 ClaimsIdentity.FindFirst() 的方法解决了我的问题。以下是使用 FakeItEasy 语法的示例,供使用该语言的任何人参考:var fakeClaim = A.Fake<Claim>(x => x.WithArgumentsForConstructor(() => new Claim(ClaimTypes.NameIdentifier, FakeEmpIDString))); var fakeIdentity = A.Fake<ClaimsIdentity>(); A.CallTo(() => fakeIdentity.FindFirst(ClaimTypes.NameIdentifier)).Returns(fakeClaim); 我的设置略有不同;不能直接设置 Controller.User 属性,因此我注入了一个 ControllerContext 并将其 HttpContext.User 属性设置为 ClaimsPrinciple 对象。 - EF0
哎呀,第二个完美无缺地运行了。这是为了获取用户ID而做的,效果非常好! - Jose A
@joelmdev,如果ID被定制为长整型而不是字符串,你的第一种方法如何处理? - xabush
@avidProgrammer 我相信你只需要将 var claim = new Claim("test", "IdOfYourChoosing"); 替换为 new Claim("test", "1234567", ClaimValueTypes.Integer64);。我没有测试过,但如果不起作用,至少也应该让你走上正确的道路。 - joelmdev
非常棒且非常简单的解决方案 :-) - Rajesh Shetty
System.NotSupportedException : Invalid setup on a non-virtual (overridable in VB) member: mock => mock.HttpContext - Sinaesthetic

20

你无法设置GetUserId方法,因为它是一个扩展方法。相反,您必须在用户的IIdentity属性上设置名称。 GetUserId将使用此属性来确定UserId。

var context = new Mock<HttpContextBase>();
var mockIdentity = new Mock<IIdentity>();
context.SetupGet(x => x.User.Identity).Returns(mockIdentity.Object);
mockIdentity.Setup(x => x.Name).Returns("test_name");

查看此链接以获取更多信息:http://msdn.microsoft.com/en-us/library/microsoft.aspnet.identity.identityextensions.getuserid(v=vs.111).aspx


1
结果发现我实际上只是在做其他愚蠢的事情,这根本不是我的问题。但是你的答案是正确的。谢谢。 - AndrewPolland

5

这对我很有效

使用fakeiteasy

        var identity = new GenericIdentity("TestUsername");
        identity.AddClaim(new Claim("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier", "UserIdIWantReturned"));

        var fakePrincipal = A.Fake<IPrincipal>();
        A.CallTo(() => fakePrincipal.Identity).Returns(identity);

        var _controller = new MyController
        {
            ControllerContext = A.Fake<ControllerContext>()
        };

        A.CallTo(() => _controller.ControllerContext.HttpContext.User).Returns(fakePrincipal); 

现在,您可以从这些 IIdentity 扩展方法中获取值:
例如:
        var userid = _controller.User.Identity.GetUserId();
        var userName = _controller.User.Identity.GetUserName();

使用ILSpy查看GetUserId()方法时,会显示:
public static string GetUserId(this IIdentity identity)
{
  if (identity == null)
  {
    throw new ArgumentNullException("identity");
  }
  ClaimsIdentity claimsIdentity = identity as ClaimsIdentity;
  if (claimsIdentity != null)
  {
    return claimsIdentity.FindFirstValue(http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier);
  }
  return null;
}

所以GetUserId需要使用声明 "nameidentifier"。

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