2016-07-24 18 views
25

Ich habe eine ASP.NET MVC Core-Anwendung, für die ich Unit-Tests schreibe. Eine der Aktionsmethoden verwendet Benutzernamen für einige Funktionen:Mocking IPrincipal in ASP.NET Core

, die offensichtlich im Komponententest fehlschlägt. Ich schaute mich um und alle Vorschläge stammen von .NET 4.5, um HttpContext nachzuahmen. Ich bin mir sicher, dass es einen besseren Weg dafür gibt. Ich habe versucht, IPrincipal zu injizieren, aber es hat einen Fehler verursacht; und ich dies sogar versucht, (aus Verzweiflung, nehme ich an):

public IActionResult Index(IPrincipal principal = null) { 
    IPrincipal user = principal ?? User; 
    SettingsViewModel svm = _context.MySettings(user.Identity.Name); 
    return View(svm); 
} 

aber warf auch einen Fehler. Konnte nichts in den Dokumenten auch finden ...

Antwort

46

Der Controller aussehen könnte User durch die HttpContext der Steuerung zugegriffen wird. Letztere wird innerhalb der ControllerContext gespeichert.

Der einfachste Weg, den Benutzer zu ersetzen, ist, einen anderen HttpContext mit einem konstruierten Benutzer zuzuweisen.Wir können DefaultHttpContext für diesen Zweck verwenden, auf diese Weise müssen Sie nicht alles verspotten:

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

In meinem Fall war 'new Claim (ClaimTypes.Name," 1 ")' passend zur Controller-Verwendung von 'user.Identity.Name'; aber ansonsten wollte ich genau das erreichen ... Danke schon! – Felix

+0

Nach unzähligen Stunden Suche war das der Post, der mich endlich zum Quadrat brachte. In meiner Core 2.0-Projekt-Controller-Methode habe ich 'User.FindFirstValue (ClaimTypes.NameIdentifier);' verwendet, um die userId für ein Objekt festzulegen, das ich erstellt habe und fehlgeschlagen ist, weil der Prinzipal null war. Das hat das für mich behoben. Danke für die tolle Antwort! –

2

Ich würde schauen, um ein abstraktes Fabrik-Muster zu implementieren.

Erstellen Sie eine Schnittstelle für eine Fabrik speziell für die Bereitstellung von Benutzernamen.

Dann bieten konkrete Klassen, eine, die User.Identity.Name bietet, und eine, die einen anderen hart codierten Wert bietet, der für Ihre Tests funktioniert.

Sie können dann die entsprechende Betonklasse abhängig von Produktion und Testcode verwenden. Vielleicht, um die Fabrik als Parameter zu übergeben oder um auf der Grundlage einiger Konfigurationswerte zur richtigen Fabrik zu wechseln.

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

Vielen Dank. Ich mache etwas ähnliches für * meine * Objekte. Ich habe nur gehofft, dass es für so etwas wie IPrinicpal etwas "out of the box" geben würde. Aber anscheinend nicht! – Felix

+0

Darüber hinaus ist Benutzer eine Mitgliedsvariable von ControllerBase. Aus diesem Grund verspotteten die Leute in früheren Versionen von ASP.NET HttpContext und erhielten IPrincipal von dort. Man kann nicht einfach Benutzer von einer eigenständigen Klasse wie ProductionFactory – Felix

10

In früheren Versionen Sie User direkt am Regler eingestellt haben könnte, was für einige sehr einfache Unit-Tests gemacht.

Wenn Sie den Quellcode für ControllerBase betrachten, werden Sie feststellen, dass User aus HttpContext extrahiert wird.

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

und die Steuerung greift die HttpContext über ControllerContext

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

Sie werden feststellen, dass diese beiden nur Eigenschaften gelesen werden. Die gute Nachricht ist, dass die ControllerContext Eigenschaft die Einstellung des Wertes erlaubt, so dass Sie Ihren Weg finden.

Also das Ziel ist es, an diesem Objekt zu bekommen. In Core HttpContext ist abstrakt, so dass es viel einfacher ist zu verspotten.

einen Controller wie

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 
} 

Verwendung Moq Angenommen, ein Test wie dieser

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

bekommen. Vielen Dank (nochmal). In der Tat, ich kann beginnen, Sie zu bezahlen :) Ich kann diese Lösung in einem komplexeren Kontext versuchen. Ich wünschte, ich könnte beide Antworten akzeptieren! – Felix

0

Es besteht auch die Möglichkeit, die bestehenden Klassen zu verwenden, und nur Mock, wenn nötig.

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