Tengo una aplicación ASP.NET MVC Core para la que estoy escribiendo pruebas unitarias. Uno de los métodos de acción utiliza el nombre de usuario para algunas funciones:
SettingsViewModel svm = _context.MySettings(User.Identity.Name);
que obviamente falla en la prueba unitaria. Miré a mi alrededor y todas las sugerencias son de .NET 4.5 para simular HttpContext. Estoy seguro de que hay una mejor manera de hacerlo. Intenté inyectar IPrincipal, pero arrojó un error; e incluso probé esto (por desesperación, supongo):
public IActionResult Index(IPrincipal principal = null) {
IPrincipal user = principal ?? User;
SettingsViewModel svm = _context.MySettings(user.Identity.Name);
return View(svm);
}
pero esto arrojó un error también. Tampoco se pudo encontrar nada en los documentos ...
new Claim(ClaimTypes.Name, "1")
para hacer coincidir el uso del controlador deuser.Identity.Name
; pero por lo demás es exactamente lo que estaba tratando de lograr ... ¡Danke schon!User.FindFirstValue(ClaimTypes.NameIdentifier);
para establecer el userId en un objeto que estaba creando y estaba fallando porque el principal era nulo. Esto arregló eso para mí. ¡Gracias por la gran respuesta!En versiones anteriores, podría haberlo configurado
User
directamente en el controlador, lo que hizo que algunas pruebas unitarias fueran muy fáciles.Si observa el código fuente de ControllerBase , notará que
User
se extrae deHttpContext
./// <summary> /// Gets the <see cref="ClaimsPrincipal"/> for user associated with the executing action. /// </summary> public ClaimsPrincipal User => HttpContext?.User;
y el controlador accede a la
HttpContext
víaControllerContext
/// <summary> /// Gets the <see cref="Http.HttpContext"/> for the executing action. /// </summary> public HttpContext HttpContext => ControllerContext.HttpContext;
Notará que estas dos son propiedades de solo lectura. La buena noticia es que la
ControllerContext
propiedad permite establecer su valor para que sea su entrada.Entonces el objetivo es llegar a ese objeto. En Core
HttpContext
es abstracto, por lo que es mucho más fácil burlarse.Asumiendo un 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 Moq, una prueba podría verse así
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); }
fuente
También existe la posibilidad de usar las clases existentes y simularlas solo cuando sea necesario.
var user = new Mock<ClaimsPrincipal>(); _controller.ControllerContext = new ControllerContext { HttpContext = new DefaultHttpContext { User = user.Object } };
fuente
En mi caso, necesitaba hacer uso de
Request.HttpContext.User.Identity.IsAuthenticated
,Request.HttpContext.User.Identity.Name
y algo de lógica de negocios sentado fuera del controlador. Pude usar una combinación de la respuesta de Nkosi, Calin y Poke para esto: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();
fuente
Buscaría implementar un patrón de fábrica abstracto.
Cree una interfaz para una fábrica específicamente para proporcionar nombres de usuario.
Luego, proporcione clases concretas, una que proporcione
User.Identity.Name
y otra que proporcione algún otro valor codificado que funcione para sus pruebas.A continuación, puede utilizar la clase de hormigón adecuada según la producción frente al código de prueba. Quizás esté buscando pasar la fábrica como parámetro, o cambiar a la fábrica correcta en función de algún valor de configuración.
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());
fuente
Quiero tocar mis controladores directamente y usar DI como AutoFac. Para hacer esto primero me registro
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);
A continuación, habilito la inyección de propiedades cuando registro los controladores.
builder.RegisterAssemblyTypes(assembly) .Where(t => t.Name.EndsWith("Controller")).PropertiesAutowired();
Luego
User.Identity.Name
se completa y no necesito hacer nada especial al llamar a un método en mi controlador.public async Task<ActionResult<IEnumerable<Employee>>> Get() { var requestedBy = User.Identity?.Name; ..................
fuente