Eu tenho um aplicativo ASP.NET MVC Core para o qual estou escrevendo testes de unidade. Um dos métodos de ação usa o nome de usuário para alguma funcionalidade:
SettingsViewModel svm = _context.MySettings(User.Identity.Name);
que obviamente falha no teste de unidade. Eu olhei em volta e todas as sugestões são do .NET 4.5 para simular HttpContext. Tenho certeza de que existe uma maneira melhor de fazer isso. Tentei injetar IPrincipal, mas gerou um erro; e eu até tentei isso (por desespero, suponho):
public IActionResult Index(IPrincipal principal = null) {
IPrincipal user = principal ?? User;
SettingsViewModel svm = _context.MySettings(user.Identity.Name);
return View(svm);
}
mas isso gerou um erro também. Também não foi possível encontrar nada nos documentos ...
new Claim(ClaimTypes.Name, "1")
para corresponder ao uso do controladoruser.Identity.Name
; mas fora isso é exatamente o que eu estava tentando alcançar ... Danke schon!User.FindFirstValue(ClaimTypes.NameIdentifier);
para definir o userId em um objeto que estava criando e estava falhando porque o principal era nulo. Isso consertou isso para mim. Obrigado pela ótima resposta!Nas versões anteriores, você poderia ter configurado
User
diretamente no controlador, o que tornava alguns testes de unidade muito fáceis.Se você olhar o código-fonte do ControllerBase , notará que o
User
foi extraídoHttpContext
./// <summary> /// Gets the <see cref="ClaimsPrincipal"/> for user associated with the executing action. /// </summary> public ClaimsPrincipal User => HttpContext?.User;
e o controlador acessa a
HttpContext
viaControllerContext
/// <summary> /// Gets the <see cref="Http.HttpContext"/> for the executing action. /// </summary> public HttpContext HttpContext => ControllerContext.HttpContext;
Você notará que essas duas são propriedades somente leitura. A boa notícia é que a
ControllerContext
propriedade permite definir seu valor para que seja o seu caminho.Portanto, o objetivo é chegar a esse objeto. No Core
HttpContext
é abstrato, então é muito mais fácil de zombar.Assumindo um controlador como
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 }
Usando o Moq, um teste poderia ser assim
public void Given_User_Index_Should_Return_ViewResult_With_Model() { //Arrange var username = "FakeUserName"; var identity = new GenericIdentity(username, ""); var mockPrincipal = new Mock<ClaimsPrincipal>(); 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); }
fonte
Também existe a possibilidade de usar as classes existentes, e simular apenas quando necessário.
var user = new Mock<ClaimsPrincipal>(); _controller.ControllerContext = new ControllerContext { HttpContext = new DefaultHttpContext { User = user.Object } };
fonte
No meu caso, eu precisava fazer uso
Request.HttpContext.User.Identity.IsAuthenticated
,Request.HttpContext.User.Identity.Name
e alguma lógica de negócios sentado fora do controlador. Consegui usar uma combinação das respostas de Nkosi, Calin e Poke para isso: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();
fonte
Eu procuraria implementar um Abstract Factory Pattern.
Crie uma interface para uma fábrica especificamente para fornecer nomes de usuário.
Em seguida, forneça classes concretas, uma que forneça
User.Identity.Name
e outra que forneça algum outro valor embutido em código que funcione para seus testes.Você pode então usar a classe concreta apropriada dependendo da produção versus código de teste. Talvez procurando passar a fábrica como um parâmetro ou alternando para a fábrica correta com base em algum valor de configuração.
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());
fonte
Eu quero acertar meus controladores diretamente e apenas usar DI como AutoFac. Para fazer isso, primeiro me inscrevo
ContextController
.var identity = new GenericIdentity("Test User"); var httpContext = new DefaultHttpContext() { User = new GenericPrincipal(identity, null) }; var context = new ControllerContext { HttpContext = httpContext}; builder.RegisterInstance(context);
Em seguida, habilito a injeção de propriedade ao registrar os controladores.
builder.RegisterAssemblyTypes(assembly) .Where(t => t.Name.EndsWith("Controller")).PropertiesAutowired();
Em seguida,
User.Identity.Name
é preenchido e não preciso fazer nada de especial ao chamar um método no meu controlador.public async Task<ActionResult<IEnumerable<Employee>>> Get() { var requestedBy = User.Identity?.Name; ..................
fonte