2014-09-30 18 views
5

Piszę test jednostkowy dla aplikacji internetowej MVC 5. Kpiłam z testu HttpContext.Current. Kiedy uruchomić następujące postaci kodu testy httpSessionStateAfter rzutHttpContext.Current ma wartość zero po oczekiwaniu (tylko w testach jednostkowych)

System.AggregateException: Wystąpił jeden lub więcej błędów.
----> System.NullReferenceException: Odwołanie do obiektu nie jest ustawione na wystąpienie obiektu.

Dzieje się tak tylko po przeprowadzeniu testów jednostkowych. Po uruchomieniu aplikacji to działa dobrze. Używam Nunit 2.6.3 z rehaper test runner.

var httpSessionStateBefour = System.Web.HttpContext.Current.Session; 
var Person= await Db.Persons.FirstOrDefaultAsync(); 
var httpSessionStateAfter = System.Web.HttpContext.Current.Session; 

Jak rozwiązać ten problem?

To jak ja mock HttpContext

HttpContext.Current = Fakes.FakeHttpContext();    
HttpContext.Current.Session.Add("IsUserSiteAdmin", true); 
HttpContext.Current.Session.Add("CurrentSite", null); 

public static class Fakes 
{ 
    public static HttpContext FakeHttpContext() 
    { 
     var httpRequest = new HttpRequest("", "http://stackoverflow/", ""); 
     var stringWriter = new StringWriter(); 
     var httpResponce = new HttpResponse(stringWriter); 
     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}); 

     return httpContext; 
    } 
} 
+1

Jakie jest dokładnie twoje pytanie? –

+0

@Tragedian Zgaduję, "dlaczego jest' httpSessionStateAfter' null, ale 'httpSessionStateBefour' nie jest?" – DavidG

+2

Które ramy testowania jednostkowego używasz? Czy na pewno obsługuje konteksty wykonania? Starsze wersje NUnit na przykład nic nie wiedzą o 'Czekaj', więc nie mogą skonfigurować kontynuacji. W aplikacjach ASP.NET kontynuacje są wykonywane na * innym * wątku wątku, który zostanie skonfigurowany z kontekstem wykonania oryginalnego. –

Odpowiedz

8

HttpContext.Current jest uważany za dość straszny własności do pracy z; nie zachowuje się poza domem ASP.NET. Najlepszym sposobem na naprawienie kodu jest zaprzestanie patrzenia na tę właściwość i znalezienie sposobu wyizolowania jej z testowanego kodu. Na przykład można utworzyć interfejs reprezentujący dane bieżącej sesji i udostępnić ten interfejs testowanemu komponentowi, z implementacją wymagającą kontekstu HTTP.


Główny problem związany jest z działaniem HttpContext.Current. Ta właściwość jest "magiczna" w środowisku ASP.NET, ponieważ jest unikalna dla operacji żądanie-odpowiedź, ale przeskakuje między wątkami, ponieważ wymaga tego wykonanie - jest selektywnie współużytkowana między wątkami.

Gdy używasz HttpContext.Current poza rurociągiem przetwarzania ASP.NET, magia znika. Po przełączeniu wątków takich jak tutaj, z asynchronicznym stylem programowania, właściwość ta jest kontynuowana po upływie null.

Jeśli absolutnie nie możesz zmienić kodu, aby usunąć trudną zależność od HttpContext.Current, możesz oszukać ten test, wykorzystując kontekst lokalny: wszystkie zmienne w zasięgu lokalnym, gdy deklarujesz kontynuację, są udostępniane dla kontekstu kontynuacji .

// Bring the current value into local scope. 
var context = System.Web.HttpContext.Current; 

var httpSessionStateBefore = context.Session; 
var person = await Db.Persons.FirstOrDefaultAsync(); 
var httpSessionStateAfter = context.Session; 

Żeby było jasne, to będzie tylko praca dla swojej obecnej sytuacji. Jeśli wprowadzisz przed sobą inny kod w postaci await, kod zostanie nagle przerwany; jest to szybka i brudna odpowiedź, którą zachęcam do zignorowania i opracowania bardziej niezawodnego rozwiązania.

+0

Najpierw robię rodzaj testów integracyjnych. Piszę test dla kontrolera. Przejdzie on do bazy danych. Użyłem HttpContext.Current, aby uzyskać szczegóły z Sesji wewnątrz DbContext. Wcześniej, co robię to wewnątrz metody SaveChanges(), która wykonuje się po niektórych wywołaniach asynchronicznych. Zamiast tego teraz dodaję go jako zainicjalizowaną właściwość konstruktora, który wykonuje się przed wywołaniami asynchronicznymi. Dziękujemy za pomoc –

9

Po pierwsze, zalecam odizolowanie kodu tak bardzo, jak to możliwe od HttpContext.Current; nie tylko spowoduje, że twój kod będzie bardziej testowalny, ale pomoże ci przygotować się do ASP.NET vNext, który jest bardziej podobny do OWIN (bez HttpContext.Current).

Jednak może to wymagać wielu zmian, na które być może nie jesteś jeszcze gotowy. Aby poprawnie kpić HttpContext.Current, musisz zrozumieć, jak to działa.

HttpContext.Current to zmienna na wątek kontrolowana przez ASP.NET SynchronizationContext.Ten SynchronizationContext jest "kontekstem żądania", reprezentującym bieżące żądanie; jest tworzony przez ASP.NET, gdy pojawia się nowe żądanie. Mam MSDN article on SynchronizationContext, jeśli chcesz uzyskać więcej szczegółów.

Jak wyjaśniam w mojej async intro blog post, kiedy awaitTask, domyślnie będzie uchwycić bieżący „kontekst” i używać, aby wznowić metodę async. Gdy metoda async działa w kontekście żądania ASP.NET, "kontekst" przechwycony przez await to ASP.NET SynchronizationContext. Po wznowieniu metody async (prawdopodobnie na innym wątku) program ASP.NET SynchronizationContext ustawi HttpContext.Current przed wznowieniem metody async. Tak działa async/await w hoście ASP.NET.

Teraz, po uruchomieniu tego samego kodu w teście jednostki, zachowanie jest inne. W szczególności nie ma programu ASP.NET SynchronizationContext do ustawienia HttpContext.Current. Zakładam, że twoja metoda testu jednostkowego zwraca Task, w którym to przypadku NUnit w ogóle nie dostarcza SynchronizationContext. Tak więc, po wznowieniu metody async (prawdopodobnie na innym wątku), jej numer HttpContext.Current może nie być taki sam.

Istnieje kilka sposobów na rozwiązanie tego problemu. Jedną z opcji jest napisanie własnego SynchronizationContext, który zachowuje HttpContext.Current, tak jak robi to program ASP.NET. Łatwiejszą (ale mniej wydajną) opcją jest użycie a SynchronizationContext that I wrote called AsyncContext, która zapewnia, że ​​metoda async zostanie wznowiona w tym samym wątku. Powinieneś być w stanie zainstalować my AsyncEx library from NuGet, a następnie owiń swoje metody testowania jednostek w rozmowie na AsyncContext.Run. Należy zauważyć, że metody badań są obecnie jednostka synchroniczna:

[Test] 
public void MyTest() 
{ 
    AsyncContext.Run(async() => 
    { 
    // Test logic goes here: set HttpContext.Current, etc. 
    }); 
} 
+0

Czy twój "Asynchroniczny kontekst" działa również dla zagnieżdżonego 'czekają'? Po prostu wypróbowałem to w projekcie .NET 4.5, w którym moja metoda testowa ustawia 'HttpContext.Current' na fałszywą, a następnie wywołuje metodę, która używa' await' wewnątrz.Dla najwyższego poziomu 'await' działa dobrze (i działał nawet bez' AsyncContext', ale zaraz po zagnieżdżonym 'czekaj'' 'HttpContext.Current' znów ma wartość null Zagnieżdżone' czekaj 'wywołuje metodę pochodną' FindByNameAsync () 'z klasy pochodzącej z' Microsoft.AspNet.Identity.UserManager'. – JustAMartin

+0

@JustAMartin: 'AsyncContext' gwarantuje tylko, że metoda zostanie wznowiona na tym samym * wątku *. Nie robi nic specjalnego z' HttpContext.Current' Wygląda na to, że jakiś kod ASP.NET może usuwać 'HttpContext.Current' .Jeśli twój kod używa' ConfigureAwait (false) ', co spowodowałoby skok wątku –

1

przyszedłem na to pytanie, gdy o problem w moim kod ... gdzie HttpContext.Current jest zerowy po czekają w Async MVC Akcji. Opublikuję to tutaj, ponieważ inni jak ja mogą tu wylądować.

Moja generalna rekomendacja to zgarnięcie wszystkiego, co chcesz z sesji, do zmiennej lokalnej, podobnie jak inne powyżej omówione, ale nie martw się utrzymaniem kontekstu, a zamiast tego martw się tylko chwytaniem rzeczywistych przedmiotów, które chcesz.

public async Task<ActionResult> SomeAction(SomeModel model) 
{ 
int id = (int)HttpContext.Current.Session["Id"]; 

/* Session Exists Here */ 

var somethingElseAsyncModel = await GetSomethingElseAsync(model); 

/* Session is Null Here */ 

// Do something with id, thanks to the fact we got it when we could 
} 
+0

Doceniam to i wznowiłem, ale człowiek, chciałbym, żebyś miał informacje o tym, jak uzyskać dostęp do zmiennej sesji PO oczekiwaniu – Barry

+0

@ Barry, możesz spróbować wskazać na nią zmienną (sesję), ale nie próbowałem takich. Wydaje mi się bezpieczniejsze i czystsze dla mnie po prostu chwyć to, czego potrzebuję, ale to przy założeniu, że nie będziesz modi fying sesji w większości przypadków. Ponadto, ogólnie staram się korzystać z sesji tak mało jak to możliwe. Nadużywanie sesji (myślenie globalne) może prowadzić do niedookreślonych zachowań, gdy użytkownik zacznie używać dwóch oddzielnych okien/kart przeglądarki. – Greg

+0

Muszę się zgodzić, skończyłem na tym, że podążałem za twoim modelem, aby pobrać potrzebne dane wcześniej. Pracuję nad konwersją starszego kodu, który opiera się w dużej mierze na sesji, na pewno jest to niebezpieczne na kilku poziomach. Doceniam informacje na ten temat, pomógł mi znacznie! – Barry

Powiązane problemy