Définition de HttpContext.Current.Session dans un test unitaire


185

J'ai un service Web que j'essaye de tester unitaire. Dans le service, il tire plusieurs valeurs de ce qui HttpContextsuit:

 m_password = (string)HttpContext.Current.Session["CustomerId"];
 m_userID = (string)HttpContext.Current.Session["CustomerUrl"];

dans le test unitaire, je crée le contexte à l'aide d'une simple demande de travail, comme ceci:

SimpleWorkerRequest request = new SimpleWorkerRequest("", "", "", null, new StringWriter());
HttpContext context = new HttpContext(request);
HttpContext.Current = context;

Cependant, chaque fois que j'essaie de définir les valeurs de HttpContext.Current.Session

HttpContext.Current.Session["CustomerId"] = "customer1";
HttpContext.Current.Session["CustomerUrl"] = "customer1Url";

J'obtiens une exception de référence nulle qui dit HttpContext.Current.Sessionest nulle.

Existe-t-il un moyen d'initialiser la session en cours dans le test unitaire?


Avez-vous essayé cette méthode ?
Raj Ranjhan

Utilisez HttpContextBase si vous le pouvez.
jrummell

Réponses:


105

Nous avons dû nous moquer HttpContexten utilisant un HttpContextManageret en appelant l'usine depuis notre application ainsi que les tests unitaires

public class HttpContextManager 
{
    private static HttpContextBase m_context;
    public static HttpContextBase Current
    {
        get
        {
            if (m_context != null)
                return m_context;

            if (HttpContext.Current == null)
                throw new InvalidOperationException("HttpContext not available");

            return new HttpContextWrapper(HttpContext.Current);
        }
    }

    public static void SetCurrentContext(HttpContextBase context)
    {
        m_context = context;
    }
}

Vous remplaceriez alors tous les appels à HttpContext.Currentpar HttpContextManager.Currentet aurez accès aux mêmes méthodes. Ensuite, lorsque vous testez, vous pouvez également accéder au HttpContextManageret simuler vos attentes

Voici un exemple utilisant Moq :

private HttpContextBase GetMockedHttpContext()
{
    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>();
    var urlHelper = new Mock<UrlHelper>();

    var routes = new RouteCollection();
    MvcApplication.RegisterRoutes(routes);
    var requestContext = new Mock<RequestContext>();
    requestContext.Setup(x => x.HttpContext).Returns(context.Object);
    context.Setup(ctx => ctx.Request).Returns(request.Object);
    context.Setup(ctx => ctx.Response).Returns(response.Object);
    context.Setup(ctx => ctx.Session).Returns(session.Object);
    context.Setup(ctx => ctx.Server).Returns(server.Object);
    context.Setup(ctx => ctx.User).Returns(user.Object);
    user.Setup(ctx => ctx.Identity).Returns(identity.Object);
    identity.Setup(id => id.IsAuthenticated).Returns(true);
    identity.Setup(id => id.Name).Returns("test");
    request.Setup(req => req.Url).Returns(new Uri("http://www.google.com"));
    request.Setup(req => req.RequestContext).Returns(requestContext.Object);
    requestContext.Setup(x => x.RouteData).Returns(new RouteData());
    request.SetupGet(req => req.Headers).Returns(new NameValueCollection());

    return context.Object;
}

puis pour l'utiliser dans vos tests unitaires, j'appelle cela dans ma méthode Test Init

HttpContextManager.SetCurrentContext(GetMockedHttpContext());

vous pouvez ensuite, dans la méthode ci-dessus, ajouter les résultats attendus de Session que vous vous attendez à être disponibles pour votre service Web.


1
mais cela n'utilise pas SimpleWorkerRequest
knocte

il essayait de se moquer du HttpContext afin que son SimpleWorkerRequest ait accès aux valeurs du HttpContext, il utiliserait le HttpContextFactory dans son service
Anthony Shaw

Est-il intentionnel que le champ de sauvegarde m_context ne soit renvoyé que pour un contexte fictif (lorsqu'il est défini via SetCurrentContext) et que pour le vrai HttpContext, un wrapper est créé pour chaque appel à Current?
Stephen Price

Oui, ça l'est. m_context est de type HttpContextBase et le retour de HttpContextWrapper renvoie HttpContextBase avec le HttpContext actuel
Anthony Shaw

1
HttpContextManagerserait un meilleur nom que HttpContextSourcemais je suis d'accord HttpContextFactoryest trompeur.
Professeur de programmation

298

Vous pouvez le "simuler" en créant un nouveau HttpContextcomme celui-ci:

http://www.necronet.org/archive/2010/07/28/unit-testing-code-that-uses-httpcontext-current-session.aspx

J'ai pris ce code et l'ai mis sur une classe d'assistance statique comme ceci:

public static HttpContext FakeHttpContext()
{
    var httpRequest = new HttpRequest("", "http://example.com/", "");
    var stringWriter = new StringWriter();
    var httpResponse = new HttpResponse(stringWriter);
    var httpContext = new HttpContext(httpRequest, httpResponse);

    var sessionContainer = new HttpSessionStateContainer("id", new SessionStateItemCollection(),
                                            new HttpStaticObjectsCollection(), 10, true,
                                            HttpCookieMode.AutoDetect,
                                            SessionStateMode.InProc, false);

    httpContext.Items["AspSession"] = typeof(HttpSessionState).GetConstructor(
                                BindingFlags.NonPublic | BindingFlags.Instance,
                                null, CallingConventions.Standard,
                                new[] { typeof(HttpSessionStateContainer) },
                                null)
                        .Invoke(new object[] { sessionContainer });

    return httpContext;
}

Ou au lieu d'utiliser la réflexion pour construire la nouvelle HttpSessionStateinstance, vous pouvez simplement attacher votre HttpSessionStateContainerau HttpContext(selon le commentaire de Brent M. Spell):

SessionStateUtility.AddHttpSessionStateToContext(httpContext, sessionContainer);

et ensuite vous pouvez l'appeler dans vos tests unitaires comme:

HttpContext.Current = MockHelper.FakeHttpContext();

24
J'aime mieux cette réponse que celle acceptée car changer votre code de production pour prendre en charge vos activités de test est une mauvaise pratique. Certes, votre code de production doit faire abstraction des espaces de noms tiers comme celui-ci, mais lorsque vous travaillez avec du code hérité, vous n'avez pas toujours ce contrôle ou le luxe de re-factoriser.
Sean Glover

29
Vous n'avez pas besoin d'utiliser la réflexion pour construire la nouvelle instance HttpSessionState. Vous pouvez simplement attacher votre HttpSessionStateContainer au HttpContext à l'aide de SessionStateUtility.AddHttpSessionStateToContext.
Brent M. Spell le

MockHelper est juste le nom de la classe où se trouve la méthode statique, vous pouvez utiliser le nom que vous préférez.
Milox

J'ai essayé d'implémenter votre réponse mais la session est toujours nulle. Pourriez-vous s'il vous plaît jeter un oeil à mon article stackoverflow.com/questions/23586765/… . Merci
Joe

Server.MapPath()ne fonctionnera pas si vous l'utilisez non plus.
Beurk

45

La solution Milox est meilleure que celle acceptée à mon humble avis mais j'ai eu quelques problèmes avec cette implémentation lors de la gestion des URL avec querystring .

J'ai apporté quelques modifications pour que cela fonctionne correctement avec toutes les URL et pour éviter la réflexion.

public static HttpContext FakeHttpContext(string url)
{
    var uri = new Uri(url);
    var httpRequest = new HttpRequest(string.Empty, uri.ToString(),
                                        uri.Query.TrimStart('?'));
    var stringWriter = new StringWriter();
    var httpResponse = new HttpResponse(stringWriter);
    var httpContext = new HttpContext(httpRequest, httpResponse);

    var sessionContainer = new HttpSessionStateContainer("id",
                                    new SessionStateItemCollection(),
                                    new HttpStaticObjectsCollection(),
                                    10, true, HttpCookieMode.AutoDetect,
                                    SessionStateMode.InProc, false);

    SessionStateUtility.AddHttpSessionStateToContext(
                                         httpContext, sessionContainer);

    return httpContext;
}

Cela vous permet de truquer httpContext.Session, une idée de comment faire la même chose httpContext.Application?
KyleMit

39

J'ai quelque chose à ce sujet il y a quelque temps.

Test unitaire HttpContext.Current.Session dans MVC3 .NET

J'espère que ça aide.

[TestInitialize]
public void TestSetup()
{
    // We need to setup the Current HTTP Context as follows:            

    // Step 1: Setup the HTTP Request
    var httpRequest = new HttpRequest("", "http://localhost/", "");

    // Step 2: Setup the HTTP Response
    var httpResponce = new HttpResponse(new StringWriter());

    // Step 3: Setup the Http Context
    var httpContext = new HttpContext(httpRequest, httpResponce);
    var sessionContainer = 
        new HttpSessionStateContainer("id", 
                                       new SessionStateItemCollection(),
                                       new HttpStaticObjectsCollection(), 
                                       10, 
                                       true,
                                       HttpCookieMode.AutoDetect,
                                       SessionStateMode.InProc, 
                                       false);
    httpContext.Items["AspSession"] = 
        typeof(HttpSessionState)
        .GetConstructor(
                            BindingFlags.NonPublic | BindingFlags.Instance,
                            null, 
                            CallingConventions.Standard,
                            new[] { typeof(HttpSessionStateContainer) },
                            null)
        .Invoke(new object[] { sessionContainer });

    // Step 4: Assign the Context
    HttpContext.Current = httpContext;
}

[TestMethod]
public void BasicTest_Push_Item_Into_Session()
{
    // Arrange
    var itemValue = "RandomItemValue";
    var itemKey = "RandomItemKey";

    // Act
    HttpContext.Current.Session.Add(itemKey, itemValue);

    // Assert
    Assert.AreEqual(HttpContext.Current.Session[itemKey], itemValue);
}

Fonctionne si bien et si simple ... Merci!
mggSoft

12

Si vous utilisez le framework MVC, cela devrait fonctionner. J'ai utilisé FakeHttpContext de Milox et ajouté quelques lignes de code supplémentaires. L'idée est venue de ce post:

http://codepaste.net/p269t8

Cela semble fonctionner dans MVC 5. Je n'ai pas essayé cela dans les versions antérieures de MVC.

HttpContext.Current = MockHttpContext.FakeHttpContext();

var wrapper = new HttpContextWrapper(HttpContext.Current);

MyController controller = new MyController();
controller.ControllerContext = new ControllerContext(wrapper, new RouteData(), controller);

string result = controller.MyMethod();

3
Le lien est rompu alors peut-être mettre le code ici la prochaine fois.
Rhyous

11

Vous pouvez essayer FakeHttpContext :

using (new FakeHttpContext())
{
   HttpContext.Current.Session["CustomerId"] = "customer1";       
}

Fonctionne très bien et très simple à utiliser
Beanwah

8

Dans asp.net Core / MVC 6 rc2, vous pouvez définir le HttpContext

var SomeController controller = new SomeController();

controller.ControllerContext = new ControllerContext();
controller.ControllerContext.HttpContext = new DefaultHttpContext();
controller.HttpContext.Session = new DummySession();

rc 1 était

var SomeController controller = new SomeController();

controller.ActionContext = new ActionContext();
controller.ActionContext.HttpContext = new DefaultHttpContext();
controller.HttpContext.Session = new DummySession();

https://stackoverflow.com/a/34022964/516748

Pensez à utiliser Moq

new Mock<ISession>();

7

La réponse qui a fonctionné avec moi est ce que @Anthony avait écrit, mais vous devez ajouter une autre ligne qui est

    request.SetupGet(req => req.Headers).Returns(new NameValueCollection());

donc vous pouvez utiliser ceci:

HttpContextFactory.Current.Request.Headers.Add(key, value);

2

Essaye ça:

        // MockHttpSession Setup
        var session = new MockHttpSession();

        // MockHttpRequest Setup - mock AJAX request
        var httpRequest = new Mock<HttpRequestBase>();

        // Setup this part of the HTTP request for AJAX calls
        httpRequest.Setup(req => req["X-Requested-With"]).Returns("XMLHttpRequest");

        // MockHttpContextBase Setup - mock request, cache, and session
        var httpContext = new Mock<HttpContextBase>();
        httpContext.Setup(ctx => ctx.Request).Returns(httpRequest.Object);
        httpContext.Setup(ctx => ctx.Cache).Returns(HttpRuntime.Cache);
        httpContext.Setup(ctx => ctx.Session).Returns(session);

        // MockHttpContext for cache
        var contextRequest = new HttpRequest("", "http://localhost/", "");
        var contextResponse = new HttpResponse(new StringWriter());
        HttpContext.Current = new HttpContext(contextRequest, contextResponse);

        // MockControllerContext Setup
        var context = new Mock<ControllerContext>();
        context.Setup(ctx => ctx.HttpContext).Returns(httpContext.Object);

        //TODO: Create new controller here
        //      Set controller's ControllerContext to context.Object

Et ajoutez la classe:

public class MockHttpSession : HttpSessionStateBase
{
    Dictionary<string, object> _sessionDictionary = new Dictionary<string, object>();
    public override object this[string name]
    {
        get
        {
            return _sessionDictionary.ContainsKey(name) ? _sessionDictionary[name] : null;
        }
        set
        {
            _sessionDictionary[name] = value;
        }
    }

    public override void Abandon()
    {
        var keys = new List<string>();

        foreach (var kvp in _sessionDictionary)
        {
            keys.Add(kvp.Key);
        }

        foreach (var key in keys)
        {
            _sessionDictionary.Remove(key);
        }
    }

    public override void Clear()
    {
        var keys = new List<string>();

        foreach (var kvp in _sessionDictionary)
        {
            keys.Add(kvp.Key);
        }

        foreach(var key in keys)
        {
            _sessionDictionary.Remove(key);
        }
    }
}

Cela vous permettra de tester à la fois avec la session et le cache.


1

Je cherchais quelque chose d'un peu moins invasif que les options mentionnées ci-dessus. En fin de compte, j'ai trouvé une solution de fromage, mais cela pourrait faire bouger certains un peu plus vite.

J'ai d'abord créé une classe TestSession :

class TestSession : ISession
{

    public TestSession()
    {
        Values = new Dictionary<string, byte[]>();
    }

    public string Id
    {
        get
        {
            return "session_id";
        }
    }

    public bool IsAvailable
    {
        get
        {
            return true;
        }
    }

    public IEnumerable<string> Keys
    {
        get { return Values.Keys; }
    }

    public Dictionary<string, byte[]> Values { get; set; }

    public void Clear()
    {
        Values.Clear();
    }

    public Task CommitAsync()
    {
        throw new NotImplementedException();
    }

    public Task LoadAsync()
    {
        throw new NotImplementedException();
    }

    public void Remove(string key)
    {
        Values.Remove(key);
    }

    public void Set(string key, byte[] value)
    {
        if (Values.ContainsKey(key))
        {
            Remove(key);
        }
        Values.Add(key, value);
    }

    public bool TryGetValue(string key, out byte[] value)
    {
        if (Values.ContainsKey(key))
        {
            value = Values[key];
            return true;
        }
        value = new byte[0];
        return false;
    }
}

Ensuite, j'ai ajouté un paramètre facultatif au constructeur de mon contrôleur. Si le paramètre est présent, utilisez-le pour la manipulation de session. Sinon, utilisez HttpContext.Session:

class MyController
{

    private readonly ISession _session;

    public MyController(ISession session = null)
    {
        _session = session;
    }


    public IActionResult Action1()
    {
        Session().SetString("Key", "Value");
        View();
    }

    public IActionResult Action2()
    {
        ViewBag.Key = Session().GetString("Key");
        View();
    }

    private ISession Session()
    {
        return _session ?? HttpContext.Session;
    }
}

Maintenant, je peux injecter mon TestSession dans le contrôleur:

class MyControllerTest
{

    private readonly MyController _controller;

    public MyControllerTest()
    {
        var testSession = new TestSession();
        var _controller = new MyController(testSession);
    }
}

J'aime vraiment ta solution. KISS => Keep It Simple and Stupid ;-)
CodeNotFound

1

Ne vous moquez jamais ... jamais! La solution est assez simple. Pourquoi simuler une si belle création comme HttpContext?

Poussez la session vers le bas! (Juste cette ligne suffit à la plupart d'entre nous pour comprendre mais expliqué en détail ci-dessous)

(string)HttpContext.Current.Session["CustomerId"];c'est ainsi que nous y accédons maintenant. Remplacez ceci par

_customObject.SessionProperty("CustomerId")

Lorsqu'il est appelé à partir d'un test, _customObject utilise un magasin alternatif (valeur de clé de base de données ou de cloud [ http://www.kvstore.io/] )

Mais lorsqu'il est appelé depuis l'application réelle, _customObjectutilise Session.

comment est-ce fait? eh bien ... Injection de dépendance!

Ainsi, le test peut définir la session (souterraine) puis appeler la méthode d'application comme si elle ne savait rien de la session. Ensuite, testez secrètement si le code de l'application a correctement mis à jour la session. Ou si l'application se comporte en fonction de la valeur de session définie par le test.

En fait, nous avons fini par nous moquer, même si j'ai dit: "ne vous moquez jamais". Parce que nous ne pouvions pas nous empêcher de passer à la règle suivante, "moquez-vous là où ça fait le moins mal!". Se HttpContextmoquer d' énorme ou se moquer d'une minuscule séance, qui fait le moins mal? ne me demandez pas d'où viennent ces règles. Disons simplement le bon sens. Voici une lecture intéressante sur ne pas se moquer car le test unitaire peut nous tuer


0

La réponse @Ro Hit m'a donnée m'a beaucoup aidé, mais il me manquait les informations d'identification de l'utilisateur car je devais simuler un utilisateur pour les tests unitaires d'authentification. Par conséquent, permettez-moi de décrire comment je l'ai résolu.

Selon cela , si vous ajoutez la méthode

    // using System.Security.Principal;
    GenericPrincipal FakeUser(string userName)
    {
        var fakeIdentity = new GenericIdentity(userName);
        var principal = new GenericPrincipal(fakeIdentity, null);
        return principal;
    }

puis ajoutez

    HttpContext.Current.User = FakeUser("myDomain\\myUser");

à la dernière ligne de la TestSetupméthode que vous avez effectuée, les informations d'identification de l'utilisateur sont ajoutées et prêtes à être utilisées pour les tests d'authentification.

J'ai également remarqué qu'il existe d'autres parties de HttpContext dont vous pourriez avoir besoin, telles que la .MapPath()méthode. Il existe un FakeHttpContext disponible, qui est décrit ici et peut être installé via NuGet.



0

Essayez de cette façon.

public static HttpContext getCurrentSession()
  {
        HttpContext.Current = new HttpContext(new HttpRequest("", ConfigurationManager.AppSettings["UnitTestSessionURL"], ""), new HttpResponse(new System.IO.StringWriter()));
        System.Web.SessionState.SessionStateUtility.AddHttpSessionStateToContext(
        HttpContext.Current, new HttpSessionStateContainer("", new SessionStateItemCollection(), new HttpStaticObjectsCollection(), 20000, true,
        HttpCookieMode.UseCookies, SessionStateMode.InProc, false));
        return HttpContext.Current;
  }
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.