Establecer HttpContext.Current.Session en una prueba unitaria

185

Tengo un servicio web que estoy tratando de probar. En el servicio, extrae varios valores de la HttpContextmisma manera:

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

en la prueba unitaria, estoy creando el contexto usando una simple solicitud de trabajo, así:

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

Sin embargo, cada vez que intento establecer los valores de HttpContext.Current.Session

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

Obtengo una excepción de referencia nula que dice que HttpContext.Current.Sessiones nula.

¿Hay alguna forma de inicializar la sesión actual dentro de la prueba unitaria?

DaveB
fuente
¿Intentaste este método ?
Raj Ranjhan
Use HttpContextBase si puede.
jrummell

Respuestas:

105

Tuvimos que burlarnos HttpContextusando a HttpContextManagery llamando a la fábrica desde nuestra aplicación, así como a las Pruebas de Unidad

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

A continuación, reemplace las llamadas a HttpContext.Currentcon HttpContextManager.Currenty tener acceso a los mismos métodos. Luego, cuando esté probando, también puede acceder HttpContextManagery burlarse de sus expectativas

Este es un ejemplo usando 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;
}

y luego para usarlo dentro de sus pruebas unitarias, lo llamo dentro de mi método Test Init

HttpContextManager.SetCurrentContext(GetMockedHttpContext());

luego, en el método anterior, puede agregar los resultados esperados de la sesión que espera que estén disponibles para su servicio web.

Anthony Shaw
fuente
1
pero esto no usa SimpleWorkerRequest
llamado
estaba tratando de burlarse del HttpContext para que su SimpleWorkerRequest tuviera acceso a los valores del HttpContext, usaría la HttpContextFactory dentro de su servicio
Anthony Shaw
¿Es intencional que el campo de respaldo m_context solo se devuelva para un contexto simulado (cuando se establece a través de SetCurrentContext) y que para el HttpContext real, se cree un contenedor para cada llamada a Current?
Stephen Price
Sí lo es. m_context es de tipo HttpContextBase y devolver HttpContextWrapper devuelve HttpContextBase con el HttpContext actual
Anthony Shaw
1
HttpContextManagersería un nombre mejor que, HttpContextSourcepero estoy de acuerdo, HttpContextFactoryes engañoso.
Profesor de programación
298

Puedes "simularlo" creando un nuevo HttpContextcomo este:

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

Tomé ese código y lo puse en una clase auxiliar estática así:

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

O en lugar de usar la reflexión para construir la nueva HttpSessionStateinstancia, simplemente puede adjuntar su HttpSessionStateContainera la HttpContext(según el comentario de Brent M. Spell):

SessionStateUtility.AddHttpSessionStateToContext(httpContext, sessionContainer);

y luego puedes llamarlo en tus pruebas unitarias como:

HttpContext.Current = MockHelper.FakeHttpContext();
Milox
fuente
24
Me gusta esta respuesta mejor que la aceptada porque cambiar su código de producción para apoyar sus actividades de prueba es una mala práctica. De acuerdo, su código de producción debe abstraer espacios de nombres de terceros como este, pero cuando trabaja con código heredado no siempre tiene este control o el lujo de re-factorizar.
Sean Glover el
29
No tiene que usar la reflexión para construir la nueva instancia HttpSessionState. Simplemente puede adjuntar su HttpSessionStateContainer al HttpContext usando SessionStateUtility.AddHttpSessionStateToContext.
Brent M. Spell
MockHelper es solo el nombre de la clase donde está el método estático, puede usar el nombre que prefiera.
Milox
Intenté implementar su respuesta, pero la sesión aún es nula. ¿Podrías echar un vistazo a mi Post stackoverflow.com/questions/23586765/… . Gracias
Joe
Server.MapPath()no funcionará si usa esto tampoco.
Yuck
45

La solución Milox es mejor que la aceptada en mi humilde opinión, pero tuve algunos problemas con esta implementación al manejar las URL con la cadena de consulta .

Hice algunos cambios para que funcione correctamente con cualquier URL y para evitar Reflection.

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;
}
giammin
fuente
Esto le permite fingir httpContext.Session, ¿alguna idea de cómo hacer lo mismo httpContext.Application?
KyleMit
39

Me preocupa algo sobre esto hace un tiempo.

Prueba de unidad HttpContext.Current.Session en MVC3 .NET

Espero eso ayude.

[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);
}
Ro Hit
fuente
Funciona muy bien y simple ... ¡Gracias!
mggSoft
12

Si está utilizando el marco MVC, esto debería funcionar. Solía del Milox FakeHttpContext y añadí unas pocas líneas de código adicionales. La idea surgió de esta publicación:

http://codepaste.net/p269t8

Esto parece funcionar en MVC 5. No he probado esto en versiones anteriores 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();
Nimblejoe
fuente
3
El enlace está roto, así que quizás ponga el código aquí la próxima vez.
Rhyous
11

Puedes probar FakeHttpContext :

using (new FakeHttpContext())
{
   HttpContext.Current.Session["CustomerId"] = "customer1";       
}
vAD
fuente
Funciona muy bien y muy sencillo de usar
Beanwah
8

En asp.net Core / MVC 6 rc2 puede configurar el HttpContext

var SomeController controller = new SomeController();

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

rc 1 era

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

Considere usar Moq

new Mock<ISession>();
KCD
fuente
7

La respuesta que funcionó conmigo es lo que @Anthony había escrito, pero debe agregar otra línea que es

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

para que puedas usar esto:

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

Prueba esto:

        // 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

Y agregue la clase:

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

Esto le permitirá probar con sesión y caché.

Isaac Alvarado
fuente
1

Estaba buscando algo un poco menos invasivo que las opciones mencionadas anteriormente. Al final se me ocurrió una solución cursi, pero podría hacer que algunas personas se muevan un poco más rápido.

Primero creé una clase 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;
    }
}

Luego agregué un parámetro opcional al constructor de mi controlador. Si el parámetro está presente, úselo para la manipulación de la sesión. De lo contrario, use 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;
    }
}

Ahora puedo inyectar mi TestSession en el controlador:

class MyControllerTest
{

    private readonly MyController _controller;

    public MyControllerTest()
    {
        var testSession = new TestSession();
        var _controller = new MyController(testSession);
    }
}
Chris Hanson
fuente
Realmente me gusta tu solución. KISS => Keep It Simple and Stupid ;-)
CodeNotFound
1

Nunca te burles ... nunca! La solución es bastante simple. ¿Por qué simular una creación tan hermosa HttpContext?

Empuje la sesión hacia abajo! (Solo esta línea es suficiente para que la mayoría de nosotros la comprendamos, pero se explica en detalle a continuación)

(string)HttpContext.Current.Session["CustomerId"];así es como accedemos ahora. Cambia esto a

_customObject.SessionProperty("CustomerId")

Cuando se llama desde la prueba, _customObject utiliza una tienda alternativa (DB o valor de clave de nube [ http://www.kvstore.io/] )

Pero cuando se llama desde la aplicación real, _customObjectutiliza Session.

como se hace esto bueno ... ¡Inyección de dependencia!

Por lo tanto, la prueba puede configurar la sesión (subterránea) y luego llamar al método de aplicación como si no supiera nada sobre la sesión. Luego, la prueba comprueba en secreto si el código de la aplicación actualizó correctamente la sesión. O si la aplicación se comporta en función del valor de sesión establecido por la prueba.

En realidad, terminamos burlándonos aunque dije: "nunca te burles". Porque no pudimos evitar pasar a la siguiente regla, "¡burlarse de donde menos duele!". Burlándose de HttpContextuna sesión pequeña o enorme , ¿qué duele menos? no me preguntes de dónde vinieron estas reglas. Digamos simplemente sentido común. Aquí hay una lectura interesante sobre no burlarse, ya que la prueba unitaria puede matarnos

Nubes azules
fuente
0

La respuesta que me dio @Ro Hit me ayudó mucho, pero me faltaban las credenciales de usuario porque tenía que falsificar a un usuario para la prueba de la unidad de autenticación. Por lo tanto, permítanme describir cómo lo resolví.

De acuerdo con esto , si agrega el método

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

y luego anexar

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

hasta la última línea del TestSetupmétodo que haya terminado, las credenciales de usuario se agregan y están listas para usarse para las pruebas de autenticación.

También noté que hay otras partes en HttpContext que podría necesitar, como el .MapPath()método. Hay un FakeHttpContext disponible, que se describe aquí y se puede instalar a través de NuGet.

Mate
fuente
0

Intenta de esta manera ...

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;
  }
Ranjan Singh
fuente