Mock HttpContext.Current dans la méthode d'initialisation de test

177

J'essaie d'ajouter des tests unitaires à une application ASP.NET MVC que j'ai créée. Dans mes tests unitaires, j'utilise le code suivant:

[TestMethod]
public void IndexAction_Should_Return_View() {
    var controller = new MembershipController();
    controller.SetFakeControllerContext("TestUser");

    ...
}

Avec les aides suivantes pour simuler le contexte du contrôleur:

public static class FakeControllerContext {
    public static HttpContextBase FakeHttpContext(string username) {
        var context = new Mock<HttpContextBase>();

        context.SetupGet(ctx => ctx.Request.IsAuthenticated).Returns(!string.IsNullOrEmpty(username));

        if (!string.IsNullOrEmpty(username))
            context.SetupGet(ctx => ctx.User.Identity).Returns(FakeIdentity.CreateIdentity(username));

        return context.Object;
    }

    public static void SetFakeControllerContext(this Controller controller, string username = null) {
        var httpContext = FakeHttpContext(username);
        var context = new ControllerContext(new RequestContext(httpContext, new RouteData()), controller);
        controller.ControllerContext = context;
    }
}

Cette classe de test hérite d'une classe de base qui a les éléments suivants:

[TestInitialize]
public void Init() {
    ...
}

À l'intérieur de cette méthode, il appelle une bibliothèque (sur laquelle je n'ai aucun contrôle) qui tente d'exécuter le code suivant:

HttpContext.Current.User.Identity.IsAuthenticated

Maintenant, vous pouvez probablement voir le problème. J'ai défini le faux HttpContext contre le contrôleur mais pas dans cette méthode Init de base. Les tests unitaires / moqueries sont très nouveaux pour moi, je veux donc m'assurer de bien faire les choses. Quelle est la bonne façon pour moi de simuler le HttpContext afin qu'il soit partagé entre mon contrôleur et toutes les bibliothèques appelées dans ma méthode Init.

nfplee
la source

Réponses:

362

HttpContext.Currentrenvoie une instance de System.Web.HttpContext, qui ne s'étend pas System.Web.HttpContextBase. HttpContextBasea été ajouté plus tard pour répondre qu'il HttpContextétait difficile de se moquer. Les deux classes ne sont fondamentalement pas liées ( HttpContextWrapperest utilisé comme un adaptateur entre elles).

Heureusement, HttpContextlui-même est juste assez faux pour que vous remplaciez IPrincipal(User) et IIdentity.

Le code suivant s'exécute comme prévu, même dans une application console:

HttpContext.Current = new HttpContext(
    new HttpRequest("", "http://tempuri.org", ""),
    new HttpResponse(new StringWriter())
    );

// User is logged in
HttpContext.Current.User = new GenericPrincipal(
    new GenericIdentity("username"),
    new string[0]
    );

// User is logged out
HttpContext.Current.User = new GenericPrincipal(
    new GenericIdentity(String.Empty),
    new string[0]
    );
Richard Szalay
la source
Bravo mais comment puis-je définir cela pour un utilisateur déconnecté?
nfplee
5
@nfplee - Si vous passez une chaîne vide dans le GenericIdentityconstructeur, IsAuthenticatedretournera false
Richard Szalay
2
Cela pourrait-il être utilisé pour simuler le cache dans HttpContext?
DevDave
1
Oui, c'est possible. Merci!
DevDave
4
@CiaranG - utilise MVC HttpContextBase, qui peut être moqué. Il n'est pas nécessaire d'utiliser la solution de contournement que j'ai publiée si vous utilisez MVC. Si vous continuez, vous devez probablement exécuter le code que j'ai publié avant même de créer le contrôleur.
Richard Szalay
35

Ci-dessous Test Init fera également le travail.

[TestInitialize]
public void TestInit()
{
  HttpContext.Current = new HttpContext(new HttpRequest(null, "http://tempuri.org", null), new HttpResponse(null));
  YourControllerToBeTestedController = GetYourToBeTestedController();
}
CARLIN
la source
Je ne peux pas obtenir d'adressabilité à HTTPContext dans un projet de test séparé dans ma solution. Pouvez-vous l'obtenir en héritant d'un contrôleur?
John Peters
1
Avez-vous des références System.Webdans votre projet de test?
PUG
Oui mais mon projet est un projet MVC, est-il possible que la version MVC de System.Web ne contienne qu'un sous-ensemble de cet espace de noms?
John Peters
2
@ user1522548 (vous devez créer un compte) Assembly System.Web.dll, v4.0.0.0 a définitivement HTTPContext Je viens de vérifier mon code source.
PUG
Mon erreur, j'ai une référence dans le fichier à System.Web.MVC et PAS System.Web. Merci de votre aide.
John Peters
7

Je sais que c'est un sujet plus ancien, mais se moquer d'une application MVC pour les tests unitaires est quelque chose que nous faisons très régulièrement.

Je voulais juste ajouter mes expériences en se moquant d'une application MVC 3 utilisant Moq 4 après la mise à niveau vers Visual Studio 2013. Aucun des tests unitaires ne fonctionnait en mode débogage et HttpContext affichait "Impossible d'évaluer l'expression" en essayant de jeter un œil aux variables .

Il s'avère que Visual Studio 2013 a des problèmes pour évaluer certains objets. Pour que le débogage des applications Web simulées fonctionne à nouveau, j'ai dû vérifier le "Utiliser le mode de compatibilité gérée" dans Outils => Options => Débogage => Paramètres généraux.

Je fais généralement quelque chose comme ça:

public static class FakeHttpContext
{
    public static void SetFakeContext(this Controller controller)
    {

        var httpContext = MakeFakeContext();
        ControllerContext context =
        new ControllerContext(
        new RequestContext(httpContext,
        new RouteData()), controller);
        controller.ControllerContext = context;
    }


    private static HttpContextBase MakeFakeContext()
    {
        var context = new Mock<HttpContextBase>();
        var request = new Mock<HttpRequestBase>();
        var response = new Mock<HttpResponseBase>();
        var session = new Mock<HttpSessionStateBase>();
        var server = new Mock<HttpServerUtilityBase>();
        var user = new Mock<IPrincipal>();
        var identity = new Mock<IIdentity>();

        context.Setup(c=> c.Request).Returns(request.Object);
        context.Setup(c=> c.Response).Returns(response.Object);
        context.Setup(c=> c.Session).Returns(session.Object);
        context.Setup(c=> c.Server).Returns(server.Object);
        context.Setup(c=> c.User).Returns(user.Object);
        user.Setup(c=> c.Identity).Returns(identity.Object);
        identity.Setup(i => i.IsAuthenticated).Returns(true);
        identity.Setup(i => i.Name).Returns("admin");

        return context.Object;
    }


}

Et initier le contexte comme celui-ci

FakeHttpContext.SetFakeContext(moController);

Et appeler la méthode dans le contrôleur directement

long lReportStatusID = -1;
var result = moController.CancelReport(lReportStatusID);
aggaton
la source
Y a-t-il une bonne raison de le définir de cette façon par rapport à la réponse acceptée? Dès le départ, cela semble plus compliqué et ne semble pas offrir d'avantages supplémentaires.
kilkfoe
1
Il propose une méthode de
simulation
4

Si votre application tierce redirige en interne, il est donc préférable de se moquer de HttpContext de la manière ci-dessous:

HttpWorkerRequest initWorkerRequest = new SimpleWorkerRequest("","","","",new StringWriter(CultureInfo.InvariantCulture));
System.Web.HttpContext.Current = new HttpContext(initWorkerRequest);
System.Web.HttpContext.Current.Request.Browser = new HttpBrowserCapabilities();
System.Web.HttpContext.Current.Request.Browser.Capabilities = new Dictionary<string, string> { { "requiresPostRedirectionHandling", "false" } };
Divang
la source