Mam aplikację ASP.NET MVC Core, dla której piszę testy jednostkowe. Jedna z metod akcji używa nazwy użytkownika dla niektórych funkcji:
SettingsViewModel svm = _context.MySettings(User.Identity.Name);
co oczywiście kończy się niepowodzeniem w teście jednostkowym. Rozejrzałem się i wszystkie sugestie pochodzą z .NET 4.5, aby udawać HttpContext. Jestem pewien, że jest na to lepszy sposób. Próbowałem wstrzyknąć IPrincipal, ale spowodowało to błąd; i nawet próbowałem tego (chyba z desperacji):
public IActionResult Index(IPrincipal principal = null) {
IPrincipal user = principal ?? User;
SettingsViewModel svm = _context.MySettings(user.Identity.Name);
return View(svm);
}
ale to również spowodowało błąd. Nie udało się też znaleźć niczego w dokumentach ...
new Claim(ClaimTypes.Name, "1")
to dopasowanie użycia kontrolerauser.Identity.Name
; ale poza tym to jest dokładnie to, co chciałem osiągnąć ... Danke schon!User.FindFirstValue(ClaimTypes.NameIdentifier);
do ustawienia userId w obiekcie, który tworzyłem, i kończyło się to niepowodzeniem, ponieważ Principal był pusty. To naprawiło to dla mnie. Dzięki za świetną odpowiedź!W poprzednich wersjach można było ustawić
User
bezpośrednio na kontrolerze, co umożliwiało bardzo łatwe testy jednostkowe.Jeśli spojrzysz na kod źródłowy ControllerBase , zauważysz, że
User
plik jest wyodrębniany zHttpContext
./// <summary> /// Gets the <see cref="ClaimsPrincipal"/> for user associated with the executing action. /// </summary> public ClaimsPrincipal User => HttpContext?.User;
a kontroler uzyskuje dostęp do pliku
HttpContext
viaControllerContext
/// <summary> /// Gets the <see cref="Http.HttpContext"/> for the executing action. /// </summary> public HttpContext HttpContext => ControllerContext.HttpContext;
Zauważysz, że te dwie właściwości są tylko do odczytu. Dobrą wiadomością jest to, że
ControllerContext
właściwość pozwala na ustawienie jej wartości, tak aby była Twoja droga.Więc celem jest dotarcie do tego obiektu. W Core
HttpContext
jest abstrakcyjny, więc o wiele łatwiej jest kpić.Zakładając, że kontroler lubi
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 }
Używając Moq, test mógłby wyglądać tak
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); }
źródło
Istnieje również możliwość korzystania z istniejących klas i mockowania tylko w razie potrzeby.
var user = new Mock<ClaimsPrincipal>(); _controller.ControllerContext = new ControllerContext { HttpContext = new DefaultHttpContext { User = user.Object } };
źródło
W moim przypadku potrzebne do wykorzystania
Request.HttpContext.User.Identity.IsAuthenticated
,Request.HttpContext.User.Identity.Name
a niektóre logika siedzi poza sterownikiem. Udało mi się użyć do tego kombinacji odpowiedzi Nkosi, Calina i Poke: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();
źródło
Chciałbym zaimplementować abstrakcyjny wzór fabryki.
Utwórz interfejs dla fabryki specjalnie w celu podawania nazw użytkowników.
Następnie podaj konkretne klasy, jedną, która zapewnia
User.Identity.Name
i taką, która zapewnia inną zakodowaną wartość, która działa w twoich testach.Następnie możesz użyć odpowiedniej konkretnej klasy w zależności od kodu produkcyjnego i testowego. Być może chce przekazać fabrykę jako parametr lub przełączyć się do właściwej fabryki w oparciu o jakąś wartość konfiguracyjną.
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());
źródło
Chcę bezpośrednio uderzyć w moje kontrolery i po prostu używać DI jak AutoFac. Aby to zrobić, najpierw się rejestruję
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);
Następnie włączam wstrzykiwanie właściwości, gdy rejestruję kontrolery.
builder.RegisterAssemblyTypes(assembly) .Where(t => t.Name.EndsWith("Controller")).PropertiesAutowired();
Następnie
User.Identity.Name
jest wypełniony i nie muszę robić nic specjalnego podczas wywoływania metody na moim kontrolerze.public async Task<ActionResult<IEnumerable<Employee>>> Get() { var requestedBy = User.Identity?.Name; ..................
źródło