2016-07-24 23 views
25

Mam aplikację ASP.NET MVC Core, dla której piszę testy jednostkowe. Jedna z metod działania wykorzystuje nazwę użytkownika dla niektórych funkcji:Szydzenie IPrincipal w środowisku ASP.NET Core

SettingsViewModel svm = _context.MySettings(User.Identity.Name); 

co oczywiście kończy się niepowodzeniem w teście jednostkowym. Rozejrzałem się wokół i wszystkie sugestie pochodzą z .NET 4.5, aby pozorować HttpContext. Jestem pewien, że jest lepszy sposób na zrobienie tego. Próbowałem wstrzyknąć IPrincipal, ale spowodowało to błąd; a nawet próbowałem tego (z desperacji, jak przypuszczam):

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 można znaleźć niczego w dokumentach albo ...

Odpowiedz

46

kontrolera User jest dostępne za pośrednictwem HttpContext regulatora. Latter jest przechowywany w numerze ControllerContext.

Najprostszym sposobem na zastąpienie użytkownika jest przypisanie innego HttpContext do skonstruowanego użytkownika.Możemy użyć DefaultHttpContext do tego celu, w ten sposób nie trzeba wyśmiewać wszystko:

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

W moim przypadku było to 'nowe żądanie (ClaimTypes.Name," 1 ")', aby dopasować użycie kontrolera 'user.Identity.Name'; ale poza tym dokładnie to chciałem osiągnąć ... Danke schon! – Felix

+0

Po niezliczonych godzinach szukanie tego było postem, które w końcu doprowadziło mnie do kwadratu. W mojej głównej wersji 2.0 kontrolera projektu używałem 'User.FindFirstValue (ClaimTypes.NameIdentifier);' do ustawiania ID użytkownika na obiekcie, który tworzyłem i zawodziło, ponieważ element główny miał wartość NULL. Naprawiono to dla mnie. Dzięki za wspaniałą odpowiedź! –

2

Chciałbym wdrożyć abstrakcyjny wzór fabryczny.

Utwórz interfejs dla fabryki specjalnie do podawania nazw użytkowników.

Następnie należy podać konkretne klasy, które zapewniają User.Identity.Name, i takie, które zapewnia inne, zakodowane wartości, które sprawdzają się w testach.

Następnie można użyć odpowiedniej klasy betonu w zależności od wersji produkcyjnej i kodu testu. Być może chcesz przekazać fabrykę jako parametr lub przejść do właściwej fabryki na podstawie pewnej wartości konfiguracyjnej.

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

Dziękuję. Robię coś podobnego dla * moich * obiektów. Miałem tylko nadzieję, że dla tak powszechnej rzeczy jak IPrinicpal, pojawi się coś "po wyjęciu z pudełka". Ale widocznie nie! – Felix

+0

Dodatkowo użytkownik jest zmienną składową ControllerBase. Dlatego we wcześniejszych wersjach ASP.NET ludzie szydzili z HttpContext i otrzymywali stamtąd IPrincipal. Nie można po prostu uzyskać Użytkownika z niezależnej klasy, np. ProductionFactory – Felix

10

W poprzednich wersjach mogłeś ustawić User bezpośrednio na sterowniku, co sprawiło, dla niektórych bardzo prostych testów jednostkowych.

Jeśli spojrzysz na jego kod źródłowy dla ControllerBase, zauważysz, że User jest wyodrębniony z HttpContext.

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

a kontroler uzyskuje dostęp do HttpContext poprzez ControllerContext

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

Można zauważyć, że te dwa są tylko do odczytu właściwości. Dobrą wiadomością jest to, że właściwość ControllerContext pozwala na ustawienie jej wartości tak, aby była dostępna.

Celem jest uzyskanie tego obiektu. W rdzeniu HttpContext jest abstraktem, więc o wiele łatwiej jest udawać.

Zakładając kontroler jak

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 
} 

Korzystanie Min, test mógłby wyglądać następująco

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

Dziękuję bardzo (znowu). Właściwie mógłbym zacząć płacić :) Mogę wypróbować to rozwiązanie w bardziej złożonym kontekście. Chciałbym móc przyjąć obie odpowiedzi! – Felix

0

Istnieje również możliwość korzystania z istniejących klas, a gdy potrzeba tylko makiety.

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