Ustawienie HttpContext.Current.Session w teście jednostkowym

185

Mam serwis internetowy, który próbuję przetestować jednostkowo. W usłudze pobiera kilka HttpContextpodobnych wartości :

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

w teście jednostkowym tworzę kontekst za pomocą prostego żądania pracownika, na przykład:

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

Jednak za każdym razem, gdy próbuję ustawić wartości HttpContext.Current.Session

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

Otrzymuję wyjątek odniesienia zerowego, który mówi, że HttpContext.Current.Sessionjest zerowy.

Czy jest jakiś sposób na zainicjowanie bieżącej sesji w ramach testu jednostkowego?

DaveB
źródło
Próbowałeś tej metody ?
Raj Ranjhan,
Jeśli to możliwe, użyj HttpContextBase .
jrummell,

Odpowiedzi:

105

Musieliśmy kpić HttpContext, używając HttpContextManageri dzwoniąc do fabryki z poziomu naszej aplikacji, a także testów jednostkowych

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

Można by potem zastąpić żadnych połączeń do HttpContext.Currentz HttpContextManager.Currentoraz mieć dostęp do tych samych metod. Następnie podczas testowania możesz uzyskać dostęp do HttpContextManagerswoich oczekiwań i kpić z nich

Oto przykład z wykorzystaniem 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;
}

a następnie, aby użyć go w testach jednostkowych, nazywam to w ramach mojej metody Init Init

HttpContextManager.SetCurrentContext(GetMockedHttpContext());

następnie możesz w powyższej metodzie dodać oczekiwane wyniki z sesji, które mają być dostępne dla Twojego serwisu internetowego.

Anthony Shaw
źródło
1
ale to nie używa SimpleWorkerRequest
knocte
próbował wyśmiewać HttpContext, aby jego SimpleWorkerRequest miał dostęp do wartości z HttpContext, używałby HttpContextFactory w swojej usłudze
Anthony Shaw
Czy jest celowe, że pole zaplecza m_context jest zwracane tylko dla próbnego kontekstu (gdy jest ustawiany przez SetCurrentContext) i że dla prawdziwego HttpContext jest tworzone opakowanie dla każdego wywołania Current?
Stephen Price
Tak to jest. m_context jest typu HttpContextBase, a zwracany HttpContextWrapper zwraca HttpContextBase z bieżącym HttpContext
Anthony Shaw
1
HttpContextManagerbyłoby lepsze imię, HttpContextSourceale zgadzam się, że HttpContextFactoryjest mylące.
Profesor programowania
298

Możesz go „sfałszować”, tworząc nowy HttpContexttaki:

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

Wziąłem ten kod i umieściłem go w statycznej klasie pomocniczej w następujący sposób:

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

Lub zamiast używać refleksji do budowy nowej HttpSessionStateinstancji, możesz po prostu dołączyć HttpSessionStateContainerją do HttpContext(zgodnie z komentarzem Brenta M. Spella):

SessionStateUtility.AddHttpSessionStateToContext(httpContext, sessionContainer);

a następnie możesz to nazwać w testach jednostkowych, takich jak:

HttpContext.Current = MockHelper.FakeHttpContext();
Milox
źródło
24
Podoba mi się ta odpowiedź lepiej niż zaakceptowana, ponieważ zmiana kodu produkcyjnego w celu wspierania działań testowych jest złą praktyką. To prawda, że ​​Twój kod produkcyjny powinien wyodrębniać takie przestrzenie nazw firm zewnętrznych, ale kiedy pracujesz ze starszym kodem, nie zawsze masz tę kontrolę lub luksus do ponownego uwzględnienia.
Sean Glover
29
Nie musisz używać refleksji do konstruowania nowej instancji HttpSessionState. Możesz po prostu dołączyć swój HttpSessionStateContainer do HttpContext za pomocą SessionStateUtility.AddHttpSessionStateToContext.
Brent M. Spell,
MockHelper to tylko nazwa klasy, w której znajduje się metoda statyczna, możesz użyć dowolnej nazwy.
Milox,
Próbowałem zaimplementować twoją odpowiedź, ale sesja wciąż jest pusta. Czy mógłbyś rzucić okiem na mój post stackoverflow.com/questions/23586765/… . Dziękuję
Joe
Server.MapPath()nie będzie działać, jeśli użyjesz tego.
Fuj
45

Rozwiązanie Milox jest lepsze niż zaakceptowane IMHO, ale miałem pewne problemy z tą implementacją podczas obsługi adresów URL za pomocą kwerendy .

Wprowadziłem kilka zmian, aby działał poprawnie z dowolnymi adresami URL i aby uniknąć refleksji.

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
źródło
To pozwala ci sfałszować httpContext.Session, jakiś pomysł, jak zrobić to samo httpContext.Application?
KyleMit,
39

Zastanawiałem się nad tym jakiś czas temu.

Testowanie jednostkowe HttpContext.Current.Session w MVC3 .NET

Mam nadzieję, że to pomoże.

[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
źródło
Działa tak dobrze i prosto ... Dzięki!
mggSoft
12

Jeśli używasz frameworka MVC, powinno to działać. użyłem FakeHttpContext firmy Milox i dodałem kilka dodatkowych wierszy kodu. Pomysł przyszedł z tego postu:

http://codepaste.net/p269t8

Wygląda na to, że działa w MVC 5. Nie próbowałem tego we wcześniejszych wersjach 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
źródło
3
Link jest zepsuty, więc może następnym razem umieść tutaj kod.
Rhyous
11

Możesz spróbować FakeHttpContext :

using (new FakeHttpContext())
{
   HttpContext.Current.Session["CustomerId"] = "customer1";       
}
vAD
źródło
Działa świetnie i jest bardzo prosty w użyciu
Beanwah 14.09.16
8

W asp.net Core / MVC 6 rc2 możesz ustawić HttpContext

var SomeController controller = new SomeController();

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

rc 1 był

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

Rozważ użycie Moq

new Mock<ISession>();
KCD
źródło
7

Odpowiedź, która ze mną zadziałała, jest tym, co napisał @Anthony, ale musisz dodać kolejny wiersz

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

więc możesz użyć tego:

HttpContextFactory.Current.Request.Headers.Add(key, value);
Yzicus
źródło
2

Spróbuj tego:

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

I Dodaj klasę:

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

Umożliwi to testowanie zarówno sesji, jak i pamięci podręcznej.

Isaac Alvarado
źródło
1

Szukałem czegoś mniej inwazyjnego niż wspomniane powyżej opcje. W końcu wymyśliłem tandetne rozwiązanie, ale może sprawić, że niektórzy ludzie poruszą się trochę szybciej.

Najpierw stworzyłem klasę 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;
    }
}

Następnie dodałem opcjonalny parametr do konstruktora mojego kontrolera. Jeśli parametr jest obecny, użyj go do manipulacji sesją. W przeciwnym razie użyj 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;
    }
}

Teraz mogę wstrzyknąć moją TestSession do kontrolera:

class MyControllerTest
{

    private readonly MyController _controller;

    public MyControllerTest()
    {
        var testSession = new TestSession();
        var _controller = new MyController(testSession);
    }
}
Chris Hanson
źródło
Naprawdę podoba mi się twoje rozwiązanie. KISS => Keep Simple i głupie ;-)
CodeNotFound
1

Nigdy nie kpij ... nigdy! Rozwiązanie jest dość proste. Po co fałszować takie piękne stworzenie HttpContext?

Zepchnij sesję w dół! (Tylko ten wiersz jest wystarczający dla większości z nas, ale wyjaśniono go szczegółowo poniżej)

(string)HttpContext.Current.Session["CustomerId"];właśnie w ten sposób uzyskujemy do niego dostęp Zmień to na

_customObject.SessionProperty("CustomerId")

Podczas wywoływania z testu _customObject używa alternatywnego magazynu (DB lub wartość klucza w chmurze [ http://www.kvstore.io/] )

Ale kiedy wywoływany z prawdziwej aplikacji, _customObjectużywa Session.

jak to się robi? no cóż ... Wstrzyknięcie zależności!

Test może więc ustawić sesję (pod ziemią), a następnie wywołać metodę aplikacji, tak jakby nic nie wiedziała o sesji. Następnie test potajemnie sprawdza, czy kod aplikacji poprawnie zaktualizował sesję. Lub jeśli aplikacja zachowuje się na podstawie wartości sesji ustawionej przez test.

W rzeczywistości skończyliśmy kpić, chociaż powiedziałem: „nigdy nie kpij”. Ponieważ nie mogliśmy się powstrzymać od przejścia do następnej zasady: „kpij tam, gdzie to najmniej boli!”. Szydzisz z ogromnej, HttpContextczy szydzisz z małej sesji, co najmniej boli? nie pytaj mnie, skąd pochodzą te zasady. Powiedzmy tylko zdrowy rozsądek. Oto ciekawa lektura na temat nie kpienia, ponieważ test jednostkowy może nas zabić

Niebieskie chmury
źródło
0

Odpowiedź udzielona przez @Ro Hit bardzo mi pomogła, ale brakowało mi poświadczeń użytkownika, ponieważ musiałem sfałszować użytkownika do testów jednostki uwierzytelniającej. Dlatego pozwól mi opisać, jak to rozwiązałem.

Zgodnie z tym , jeśli dodasz metodę

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

a następnie dołącz

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

do ostatniego wiersza TestSetupmetody, którą wykonałeś, poświadczenia użytkownika są dodawane i gotowe do użycia w testach uwierzytelniania.

Zauważyłem również, że w HttpContext są inne części, których możesz potrzebować, takie jak .MapPath()metoda. Dostępny jest FakeHttpContext, który został opisany tutaj i można go zainstalować za pomocą NuGet.

Matt
źródło
0

Spróbuj w ten sposób ..

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
źródło