Mocking IPrincipal в ядре ASP.NET

У меня есть приложение 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); } 

но это тоже породило ошибку. Не удалось найти ничего в документах …

    Пользователь controllerа получает доступ через HttpContext controllerа. Последнее хранится внутри ControllerContext .

    Самый простой способ заменить пользователя – назначить другой HttpContext с созданным пользователем. Мы можем использовать DefaultHttpContext для этой цели, так что вам не нужно издеваться над всем:

     var user = new ClaimsPrincipal(new ClaimsIdentity(new Claim[] { new Claim(ClaimTypes.NameIdentifier, "1"), new Claim(MyCustomClaim, "example claim value") })); var controller = new SomeController(dependencies…); controller.ControllerContext = new ControllerContext() { HttpContext = new DefaultHttpContext() { User = user } }; 

    В предыдущих версиях вы могли установить User непосредственно на controllerе, что сделало некоторые очень легкие модульные тесты.

    Если вы посмотрите на исходный код для ControllerBase, вы заметите, что User извлечен из HttpContext .

     ///  /// Gets or sets the  for user associated with the executing action. ///  public ClaimsPrincipal User { get { return HttpContext?.User; } } 

    и controller обращается к HttpContext через ControllerContext

     ///  /// Gets the  for the executing action. ///  public HttpContext HttpContext { get { return ControllerContext.HttpContext; } } 

    Вы заметите, что эти два являются свойствами только для чтения. Хорошей новостью является то, что свойство ControllerContext позволяет устанавливать его значение, чтобы оно было вашим.

    Таким образом, цель – добраться до этого объекта. В Core HttpContext является абстрактным, поэтому его намного проще HttpContext .

    Предполагая, что controller

     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(); mockPrincipal.Setup(x => x.Identity).Returns(identity); mockPrincipal.Setup(x => x.IsInRole(It.IsAny())).Returns(true); var mockHttpContext = new Mock(); mockHttpContext.Setup(m => m.User).Returns(mockPrincipal.Object); var model = new SettingsViewModel() { //...other code removed for brevity }; var mockContext = new Mock(); 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 конкретные classы, один из которых предоставляет 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()); 

    Существует также возможность использовать существующие classы и высмеивать только при необходимости.

     var user = new Mock(); _controller.ControllerContext = new ControllerContext { HttpContext = new DefaultHttpContext { User = user.Object } }; 
    Давайте будем гением компьютера.