94 votes

Se moquant de IPrincipal dans ASP.NET de Base

J'ai un ASP.NET MVC application de Base que je suis en train d'écrire des tests unitaires pour. L'une des méthodes d'action utilise le nom d'Utilisateur pour certaines fonctionnalités:

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

qui, évidemment, ne parvient pas à l'unité de test. Je regardai autour de moi et toutes les suggestions sont les .NET 4.5, à se moquer de HttpContext. Je suis sûr qu'il y est une meilleure façon de le faire. J'ai essayé d'injecter IPrincipal, mais il a jeté une erreur; et j'ai même essayé (en désespoir de cause, je suppose):

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

mais cela a jeté une erreur. Ne pouvais pas trouver quelque chose dans les docs...

185voto

poke Points 64398

Le contrôleur User est accessible par l' HttpContext du contrôleur. Ce dernier est stocké à l'intérieur de l' ControllerContext.

La façon la plus simple pour remplacer l'utilisateur est par l'affectation d'un autre HttpContext avec un construit de l'utilisateur. On peut utiliser DefaultHttpContext à cette fin, de cette façon, vous n'avez pas à se moquer de tout:

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

17voto

Nkosi Points 95895

Dans les versions précédentes, vous pourriez avoir réglé User directement sur le contrôleur, qui a fait pour certains très facile de tests unitaires.

Si vous regardez il de code source pour ControllerBase vous remarquerez que l' User est extrait de l' HttpContext.

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

et le contrôleur accède à l' HttpContext par ControllerContext

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

Vous remarquerez que ces deux sont des propriétés en lecture seulement. La bonne nouvelle, c'est qu' ControllerContext propriété permet le réglage de la valeur, de sorte que ce sera votre façon.

Donc, l'objectif est d'obtenir à cet objet. Le Noyau de l' HttpContext est abstraite, de sorte qu'il est beaucoup plus facile de se moquer.

En supposant un contrôleur comme

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 
}

À l'aide de Moq, un test pourrait ressembler à ceci

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

3voto

Calin Points 1217

Il y a aussi la possibilité d'utiliser les classes existantes, et se moquer uniquement lorsque cela est nécessaire.

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

2voto

James Wood Points 6334

Je regarde pour mettre en œuvre un Résumé de l'Usine Modèle.

Créer une interface pour une usine spécialement pour fournir des noms d'utilisateur.

Ensuite fournir les classes de béton, qui fournit User.Identity.Name, et certaines autres codé en dur la valeur qui fonctionne pour vos tests.

Vous pouvez ensuite utiliser la classe de béton en fonction de la production de rapport de test de code. Peut-être à la recherche à passer de l'usine en tant que paramètre, ou de passer à la correcte usine basée sur la configuration de la valeur.

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

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X