2016-07-24 6 views
25

У меня есть приложение ASP.NET MVC Core, для которого я пишу модульные тесты. В одном из методов действия используется имя пользователя для некоторых функций:Mocking IPrincipal в ASP.NET 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); 
} 

но бросил ошибку, а также. Не удалось найти что-либо в документах ...

ответ

46

контроллера User доступен через HttpContext контроллера. Последнее хранится в пределах 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 } 
}; 
+5

В моем случае это было 'new Claim (ClaimTypes.Name," 1 ")' для соответствия использованию контроллера 'user.Identity.Name'; но в противном случае это именно то, чего я пытался достичь ... Danke schon! – Felix

+0

После бесчисленных часов поиска это было сообщение, которое, наконец, привело меня в квадрат. В моем основном проекте контроллера проекта я использовал «User.FindFirstValue (ClaimTypes.NameIdentifier)», чтобы установить userId для объекта, который я создавал, и терпел неудачу, потому что основной был null. Это фиксировало это для меня. Спасибо за отличный ответ! –

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()); 
+0

спасибо. Я делаю что-то подобное для * моих * объектов. Я просто надеялся, что для такой общей вещи, как IPrinicpal, будет что-то «из коробки». Но, видимо, нет! – Felix

+0

Кроме того, Пользователь является переменной-членом ControllerBase. Вот почему в ранних версиях ASP.NET люди издевались над HttpContext и получали оттуда IPrincipal. Нельзя просто получить Пользователя из автономного класса, например, ProductionFactory. – Felix

10

В предыдущих версиях вы могли бы установить 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 позволяет установить его значение так, чтобы оно было вашим.

Итак, цель - добраться до этого объекта. В 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<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); 
} 
+0

Большое спасибо (снова). На самом деле, я могу начать платить вам :) Я могу попробовать это решение в более сложном контексте. Хотел бы я согласиться на оба ответа! – Felix

0

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

var user = new Mock<ClaimsPrincipal>(); 
_controller.ControllerContext = new ControllerContext 
{ 
    HttpContext = new DefaultHttpContext 
    { 
     User = user.Object 
    } 
}; 
Смежные вопросы