在ASP.NET Core中模拟IPrincipal

问题描述 投票:58回答:6

我有一个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);
}

但这也引发了一个错误。无法在文档中找到任何内容......

c# unit-testing asp.net-core-mvc xunit.net
6个回答
123
投票

控制器的User is accessed通过HttpContext of the controller。后者is storedControllerContext内。

设置用户的最简单方法是为构造用户分配不同的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正常工作(如果您在代码中使用它来确定用户是否经过身份验证)。


15
投票

在以前的版本中,您可以直接在控制器上设置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);
}

2
投票

我希望实现一个抽象工厂模式。

为工厂创建专门用于提供用户名的界面。

然后提供具体的类,一个提供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());

2
投票

还可以使用现有的类,并仅在需要时进行模拟。

var user = new Mock<ClaimsPrincipal>();
_controller.ControllerContext = new ControllerContext
{
    HttpContext = new DefaultHttpContext
    {
        User = user.Object
    }
};

2
投票

就我而言,我需要使用Request.HttpContext.User.Identity.IsAuthenticatedRequest.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();

0
投票

您可以使用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

© www.soinside.com 2019 - 2024. All rights reserved.