questions net mvc examen exam developing asp applications c# unit-testing asp.net-core-mvc xunit.net

c# - net - exam 70-486



Burlándose de IPrincipal en ASP.NET Core (6)

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 burlarse de HttpContext. Estoy seguro de que hay una mejor manera de hacerlo. Traté de inyectar IPrincipal, pero arrojó un error; e incluso intenté 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. No pude encontrar nada en los documentos tampoco ...


En mi caso, necesitaba hacer uso de Request.HttpContext.User.Identity.IsAuthenticated , Request.HttpContext.User.Identity.Name y algo de lógica de negocios 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();


En versiones anteriores, podría haber configurado User directamente en el controlador, lo que hizo algunas pruebas unitarias muy fáciles.

Si observa el código fuente de ControllerBase , notará que el User se extrae de HttpContext .

/// <summary> /// Gets or sets the <see cref="ClaimsPrincipal"/> for user associated with the executing action. /// </summary> public ClaimsPrincipal User { get { return HttpContext?.User; } }

y el controlador accede al HttpContext través de ControllerContext

/// <summary> /// Gets the <see cref="Http.HttpContext"/> for the executing action. /// </summary> public HttpContext HttpContext { get { return ControllerContext.HttpContext; } }

Notará que estas dos son propiedades de solo lectura. La buena noticia es que la propiedad ControllerContext permite establecer su valor para que sea su camino.

Entonces el objetivo es llegar a ese objeto. En Core HttpContext es abstracto, por lo que es mucho más fácil burlarse de él.

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<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); }


Me gustaría implementar un patrón abstracto de fábrica.

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.

Luego puede usar la clase concreta apropiada dependiendo de la producción versus el código de prueba. Quizás busque 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());


Puede burlarse de HttpContext en Net Core usando IHttpContextAccessor, de esta manera:

public class UserRepository : IUserRepository { private readonly IHttpContextAccessor _httpContextAccessor; public UserRepository(IHttpContextAccessor httpContextAccessor) { _httpContextAccessor = httpContextAccessor; } public void LogCurrentUser() { var username = _httpContextAccessor.HttpContext.User.Identity.Name; service.LogAccessRequest(username); } }

Esto está tomado de esta página: https://docs.microsoft.com/en-us/aspnet/core/fundamentals/http-context?view=aspnetcore-2.2


Se accede al User del controlador a través del HttpContext del controlador . Este último se almacena en el ControllerContext .

La forma más fácil de configurar al usuario es asignando un HttpContext diferente con un usuario construido. Podemos usar DefaultHttpContext para este propósito, de esa manera no tenemos que burlarnos de todo. Luego solo usamos ese HttpContext dentro de un contexto de controlador y lo pasamos a la instancia del controlador:

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

Al crear su propia ClaimsIdentity , asegúrese de pasar un tipo de authenticationType explícito al constructor. Esto asegura que IsAuthenticated funcionará correctamente (en caso de que use eso en su código para determinar si un usuario está autenticado).


También existe la posibilidad de usar las clases existentes, y simulacros solo cuando sea necesario.

var user = new Mock<ClaimsPrincipal>(); _controller.ControllerContext = new ControllerContext { HttpContext = new DefaultHttpContext { User = user.Object } };