c# - Configuración de HttpContext.Current.Session en una prueba de unidad
web-services unit-testing (13)
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
Considera usar Moq
new Mock<ISession>();
Tengo un servicio web que estoy intentando probar en una unidad. En el servicio, extrae varios valores del HttpContext
así:
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, como esta:
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.Session
es nulo.
¿Hay alguna manera de inicializar la sesión actual dentro de la prueba unitaria?
Encontré la siguiente solución simple para especificar un usuario en HttpContext: https://forums.asp.net/post/5828182.aspx
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);
}
}
Hice algo al respecto hace un tiempo.
Prueba unitaria 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);
}
La respuesta @Ro hit me ayudó mucho, pero me faltaban las credenciales de usuario porque tuve que falsificar a un usuario para probar la unidad de autenticación. Por lo tanto, permítanme describir cómo lo resolví.
De acuerdo con this , 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");
a la última línea del método TestSetup
que haya terminado, las credenciales del 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 método .MapPath()
. Hay un FakeHttpContext disponible, que se describe aquí y se puede instalar a través de NuGet.
Nunca te burles ... ¡nunca! La solución es bastante simple. ¿Por qué falsificar una creación tan hermosa como HttpContext
?
¡Empuja la sesión hacia abajo! (Solo esta línea es suficiente para que la mayoría de nosotros la comprenda, pero se explica en detalle a continuación)
(string)HttpContext.Current.Session["CustomerId"];
es cómo accedemos ahora. Cambiar esto a
_customObject.SessionProperty("CustomerId")
Cuando se llama desde la prueba, _customObject usa una tienda alternativa (DB o valor de clave en la nube [ http://www.kvstore.io/] )
Pero cuando se llama desde la aplicación real, _customObject
usa Session
.
¿Cómo se hace esto? bien ... Inyección de dependencia!
Por lo tanto, la prueba puede establecer la sesión (bajo tierra) y llamar a un método como si no supiera nada sobre la sesión. La prueba decide cuándo y qué se almacena en la sesión.
En realidad, terminamos burlándonos a pesar de que dije: "nunca te burles". Porque no pudimos evitar pasar a la siguiente regla, "¡fingir dónde duele menos!". Burlarse de HttpContext
o burlarse de una pequeña sesión, ¿qué duele menos? no me preguntes de dónde vienen estas reglas. Creo que estas reglas son puro sentido común. Aquí hay una lectura interesante sobre no burlarse ya que la prueba unitaria puede matarnos
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 agrega 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é.
Puedes "simularlo" creando un nuevo HttpContext
como este:
Tomé ese código y lo puse en una clase de ayuda estática como esta:
public static HttpContext FakeHttpContext()
{
var httpRequest = new HttpRequest("", "http:///", "");
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 el reflejo para construir la nueva instancia de HttpSessionState
, puede simplemente adjuntar su HttpSessionStateContainer
al HttpContext
(según el comentario de Brent M. Spell):
SessionStateUtility.AddHttpSessionStateToContext(httpContext, sessionContainer);
y luego puedes llamarlo en las pruebas de tu unidad como:
HttpContext.Current = MockHelper.FakeHttpContext();
Puedes probar FakeHttpContext :
using (new FakeHttpContext())
{
HttpContext.Current.Session["CustomerId"] = "customer1";
}
Si está utilizando el marco MVC, esto debería funcionar. Utilicé FakeHttpContext de Milox y agregué algunas líneas adicionales de código. La idea vino de esta publicación:
Esto parece funcionar en MVC 5. No lo he probado 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();
Tuvimos que HttpContext
de HttpContext
utilizando un HttpContextManager
y llamando a la fábrica desde nuestra aplicación, así como a las Pruebas unitarias
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;
}
}
Luego reemplazaría cualquier llamada a HttpContext.Current
con HttpContextManager.Current
y tendrá acceso a los mismos métodos. Luego, cuando esté probando, también puede acceder al HttpContextManager
y burlarse de sus expectativas
Este es un ejemplo que usa 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 usarlo dentro de las pruebas de su unidad, lo llamo dentro de mi método Test Init
HttpContextManager.SetCurrentContext(GetMockedHttpContext());
Entonces, en el método anterior, puede agregar los resultados esperados de la sesión que espera que estén disponibles para su servicio web.
La solución Milox es mejor que la aceptada en mi humilde opinión, pero tuve algunos problemas con esta implementación cuando manejaba urls con querystring .
Hice algunos cambios para que funcione correctamente con las URL y para evitar la reflexión.
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;
}
La respuesta que funcionó conmigo es lo que @Anthony había escrito, pero tienes que 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);