Definindo HttpContext.Current.Session em um teste de unidade

185

Eu tenho um serviço da web que estou tentando fazer teste de unidade. No serviço, ele puxa vários valores do HttpContextmesmo modo:

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

no teste de unidade, estou criando o contexto usando uma simples solicitação de trabalho, como:

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

No entanto, sempre que tento definir os valores de HttpContext.Current.Session

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

Eu recebo uma exceção de referência nula que diz que HttpContext.Current.Sessioné nula.

Existe alguma maneira de inicializar a sessão atual dentro do teste de unidade?

DaveB
fonte
Você tentou este método ?
Raj Ranjhan
Use HttpContextBase, se puder.
jrummell

Respostas:

105

Tivemos que zombar HttpContextusando ae HttpContextManagerligando para a fábrica de dentro do nosso aplicativo, bem como os testes de unidade

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

Você substituiria todas as chamadas HttpContext.Currentpor HttpContextManager.Currente teria acesso aos mesmos métodos. Então, quando você estiver testando, também poderá acessar HttpContextManagere zombar de suas expectativas

Este é um exemplo usando o 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;
}

e, em seguida, para usá-lo em seus testes de unidade, eu chamo isso no meu método Test Init

HttpContextManager.SetCurrentContext(GetMockedHttpContext());

você pode, no método acima, adicionar os resultados esperados da sessão que você espera estar disponíveis para o seu serviço da web.

Anthony Shaw
fonte
1
mas isso não usa
SimpleWorkerRequest
ele estava tentando zombar o HttpContext para que sua SimpleWorkerRequest teriam acesso aos valores do HttpContext, ele usaria o HttpContextFactory dentro de seu serviço
Anthony Shaw
É intencional que o campo de apoio m_context seja retornado apenas para um contexto simulado (quando definido via SetCurrentContext) e que, para o HttpContext real, um wrapper seja criado para cada chamada para Current?
Stephen Price
Sim, ele é. m_context é do tipo HttpContextBase e retornando HttpContextWrapper retorna HttpContextBase com o atual HttpContext
Anthony Shaw
1
HttpContextManagerseria um nome melhor do que, HttpContextSourcemas concordo que HttpContextFactoryé enganoso.
Professor de programação
298

Você pode "fingir" criando um novo HttpContextcomo este:

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

Eu peguei esse código e o coloquei em uma classe auxiliar estática da seguinte forma:

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, em vez de usar a reflexão para construir a nova HttpSessionStateinstância, basta anexá HttpSessionStateContainer-la ao HttpContext(como no comentário de Brent M. Spell):

SessionStateUtility.AddHttpSessionStateToContext(httpContext, sessionContainer);

e então você pode chamá-lo em seus testes de unidade, como:

HttpContext.Current = MockHelper.FakeHttpContext();
Milox
fonte
24
Gosto mais desta resposta do que a aceita porque alterar o código de produção para dar suporte às atividades de teste é uma prática ruim. É verdade que seu código de produção deve abstrair espaços de nomes de terceiros como este, mas quando você trabalha com código legado, nem sempre tem esse controle ou o luxo de re-fatorar.
Sean Glover
29
Você não precisa usar a reflexão para construir a nova instância HttpSessionState. Você pode simplesmente anexar seu HttpSessionStateContainer ao HttpContext usando SessionStateUtility.AddHttpSessionStateToContext.
Brent M. Spell
MockHelper é apenas o nome da classe onde está o método estático, você pode usar o nome que preferir.
Milox
Tentei implementar sua resposta, mas a sessão ainda é nula. Por favor, dê uma olhada no meu Post stackoverflow.com/questions/23586765/… . Obrigado
Joe
Server.MapPath()não funcionará se você usar isso também.
Yuck
45

A solução Milox é melhor que a aceita um IMHO, mas tive alguns problemas com essa implementação ao lidar com URLs com querystring .

Fiz algumas alterações para fazê-lo funcionar corretamente com quaisquer URLs e evitar o 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
fonte
Isso permite que você finja httpContext.Session, alguma idéia de como fazer o mesmo httpContext.Application?
precisa saber é o seguinte
39

Eu torço algo sobre isso há um tempo atrás.

Teste de unidade HttpContext.Current.Session no MVC3 .NET

Espero que ajude.

[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
fonte
Funciona tão bem e simples ... Obrigado!
mggSoft
12

Se você estiver usando a estrutura MVC, isso deve funcionar. Eu costumava da Milox FakeHttpContext e acrescentou algumas linhas de código adicionais. A ideia veio deste post:

http://codepaste.net/p269t8

Isso parece funcionar no MVC 5. Não tentei isso nas versões anteriores do 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
fonte
3
O link está quebrado, então talvez seja necessário colocar o código aqui da próxima vez.
Rhyous 9/0318
11

Você pode tentar o FakeHttpContext :

using (new FakeHttpContext())
{
   HttpContext.Current.Session["CustomerId"] = "customer1";       
}
vAD
fonte
Funciona muito bem e muito simples de usar
Beanwah
8

No asp.net Core / MVC 6 rc2, você pode definir o HttpContext

var SomeController controller = new SomeController();

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

o 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
fonte
7

A resposta que funcionou comigo é o que @Anthony havia escrito, mas você precisa adicionar outra linha que seja

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

então você pode usar isso:

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

Tente o seguinte:

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

E adicione a 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);
        }
    }
}

Isso permitirá que você teste com a sessão e o cache.

Isaac Alvarado
fonte
1

Eu estava procurando algo um pouco menos invasivo do que as opções mencionadas acima. No final, eu vim com uma solução brega, mas isso pode fazer algumas pessoas se moverem um pouco mais rápido.

Primeiro, criei uma 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;
    }
}

Em seguida, adicionei um parâmetro opcional ao construtor do meu controlador. Se o parâmetro estiver presente, use-o para manipulação de sessão. Caso contrário, use o 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;
    }
}

Agora eu posso injetar minha TestSession no controlador:

class MyControllerTest
{

    private readonly MyController _controller;

    public MyControllerTest()
    {
        var testSession = new TestSession();
        var _controller = new MyController(testSession);
    }
}
Chris Hanson
fonte
Eu realmente gosto da sua solução. BEIJO => Keep It Simple e estúpido ;-)
CodeNotFound
1

Nunca zombe .. nunca! A solução é bem simples. Por que fingir uma criação tão bonita como HttpContext?

Empurre a sessão para baixo! (Apenas esta linha é suficiente para a maioria de nós entender, mas explicada em detalhes abaixo)

(string)HttpContext.Current.Session["CustomerId"];é como acessamos agora. Mude para

_customObject.SessionProperty("CustomerId")

Quando chamado do teste, _customObject usa armazenamento alternativo (valor da chave do banco de dados ou nuvem [ http://www.kvstore.io/] )

Mas quando chamado a partir do aplicativo real, _customObjectusa Session.

como isso é feito? bem ... Injeção de Dependência!

Portanto, o teste pode definir a sessão (subterrânea) e, em seguida, chamar o método de aplicação como se não soubesse nada sobre a sessão. Em seguida, teste secretamente verifica se o código do aplicativo atualizou corretamente a sessão. Ou se o aplicativo se comportar com base no valor da sessão definido pelo teste.

Na verdade, acabamos zombando, embora eu tenha dito: "nunca zombe". Como não podíamos deixar de passar para a próxima regra, "zombe de onde dói menos!". Zombando imenso HttpContextou zombando de uma pequena sessão, o que dói menos? não me pergunte de onde essas regras vieram. Vamos apenas dizer bom senso. Aqui está uma leitura interessante sobre não zombar, pois o teste de unidade pode nos matar

Nuvens azuis
fonte
0

A resposta que o @Ro Hit deu me ajudou muito, mas estava faltando as credenciais do usuário porque tive que falsificar um usuário para testar a unidade de autenticação. Portanto, deixe-me descrever como eu o resolvi.

De acordo com isso , se você adicionar o método

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

e depois acrescentar

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

até a última linha do TestSetupmétodo, as credenciais do usuário são adicionadas e prontas para serem usadas no teste de autenticação.

Também notei que existem outras partes no HttpContext que você pode precisar, como o .MapPath()método Existe um FakeHttpContext disponível, que é descrito aqui e pode ser instalado via NuGet.

Matt
fonte
0

Tente assim ..

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
fonte