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); }
но это тоже породило ошибку. Не удалось найти ничего в документах …
- Модульное тестирование HTTP-запросов в c #
- Как издеваться над запросом на controller в ASP.Net MVC?
- Условно игнорирование тестов в JUnit 4
- Как мне высмеять User.Identity.GetUserId ()?
- Единичный тест на безопасность резьбы?
- Какова моя «цель» и как мне добавить файл для модульного тестирования?
- Spring @ContextConfiguration, как установить правильное местоположение для xml
- Как начать работу с GTest и CMake
- Тестирование единиц: DateTime.Now
- Публикация частного метода для тестирования объекта ... хорошая идея?
- Тестирование API Java EE 6
- Как выполнить единичный тест сохранения файла на диск?
- Сервлеты для тестирования модhive
Пользователь 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 } };