2009-07-31 12 views
35

Rozważmy metodę w zespole .NET:Moq: jednostka testowania metody polegając na HttpContext

public static string GetSecurityContextUserName() 
{    
//extract the username from request    
string sUser = HttpContext.Current.User.Identity.Name; 
//everything after the domain  
sUser = sUser.Substring(sUser.IndexOf("\\") + 1).ToLower(); 

return sUser;  
} 

Chciałbym wywołać tę metodę z badanej jednostki przy użyciu ramy Min. Ten zespół jest częścią rozwiązania formularzy internetowych. Test jednostkowy wygląda tak, ale brakuje mi kodu Moq.

//arrange 
string ADAccount = "BUGSBUNNY"; 
string fullADName = "LOONEYTUNES\BUGSBUNNY"; 

//act  
//need to mock up the HttpContext here somehow -- using Moq. 
string foundUserName = MyIdentityBL.GetSecurityContextUserName(); 

//assert 
Assert.AreEqual(foundUserName, ADAccount, true, "Should have been the same User Identity."); 

Pytanie:

  • Jak mogę używać Min zorganizować sztuczny obiekt HttpContext z jakąś wartość jak 'MojaDomena \ myuser'?
  • Jak skojarzyć ten fałszywy z moim wywołaniem w mojej statycznej metody pod numerem MyIdentityBL.GetSecurityContextUserName()?
  • Czy masz jakieś sugestie, jak poprawić ten kod/architekturę?

Odpowiedz

39

Formularze internetowe są notorycznie niepoprawne z tego konkretnego powodu - wiele kodu może polegać na klasach statycznych w potoku asp.net.

Aby przetestować to za pomocą Moq, należy zmienić metodę GetSecurityContextUserName(), aby użyć iniekcji zależności za pomocą obiektu HttpContextBase.

HttpContextWrapper znajduje się w System.Web.Abstractions, który jest dostarczany z .Net 3.5. Jest to wrapper dla klasy HttpContext i rozciąga HttpContextBase, można skonstruować HttpContextWrapper właśnie tak:

var wrapper = new HttpContextWrapper(HttpContext.Current); 

Nawet lepiej, można drwić HttpContextBase i skonfigurować go za pomocą oczekiwania na Min. W tym zalogowany użytkownik itd

var mockContext = new Mock<HttpContextBase>(); 

Mając to na miejscu, można nazwać GetSecurityContextUserName(mockContext.Object), a aplikacja jest znacznie mniej sprzężony statycznych WebForms HTTPContext. Jeśli masz zamiar wykonać wiele testów opartych na wyśmianym kontekście, bardzo polecam taking a look at Scott Hanselman's MvcMockHelpers class, która ma wersję do użycia z Moq. Dogodnie obsługuje wiele niezbędnych ustawień. I pomimo nazwy, nie musisz tego robić z MVC - używam go z powodzeniem w aplikacjach internetowych, kiedy mogę je zrestrukturyzować, aby używać HttpContextBase.

+0

nie można używać nowego HttpContextBase(), jak jest to abstrakcyjna klasa (jak sama nazwa wskazuje). Musisz użyć nowej metody HttpContextWrapper(). –

+0

Dzięki ... mój mózg był przed moich rąk, kiedy odpowiedziałem, że to;) – womp

0

Jeśli używasz modelu zabezpieczeń CLR (tak jak my to robimy), musisz użyć niektórych abstrakcyjnych funkcji, aby pobrać i ustawić bieżącą nazwę użytkownika, jeśli chcesz zezwolić na testowanie i używać ich przy każdym pobieraniu lub ustawianiu dyrektor. Dzięki temu można uzyskać/ustawić głównego, gdziekolwiek jest to istotne (zazwyczaj na stronie internetowej HttpContext, a także w bieżącym wątku w innym miejscu, np. W testach jednostkowych). To będzie wyglądać następująco:

public static IPrincipal GetCurrentPrincipal() 
{ 
    return HttpContext.Current != null ? 
     HttpContext.Current.User : 
     Thread.CurrentThread.Principal; 
} 

public static void SetCurrentPrincipal(IPrincipal principal) 
{ 
    if (HttpContext.Current != null) HttpContext.Current.User = principal' 
    Thread.CurrentThread.Principal = principal; 
} 

Jeśli używasz niestandardowego kapitału następnie mogą one być dość ładnie zintegrowany interfejs, na przykład poniżej Current nazwałbym GetCurrentPrincipal i SetAsCurrent nazwałbym SetCurrentPrincipal.

public class MyCustomPrincipal : IPrincipal 
{ 
    public MyCustomPrincipal Current { get; } 
    public bool HasCurrent { get; } 
    public void SetAsCurrent(); 
} 
+0

Ten kod jest niepotrzebny, ponieważ jeśli zastanowić się nad kodem ASP.NET zobaczysz, że HttpContext.Current.User i Thread.CurrentThread.Principal są zawsze ustawione na tę samą wartość, więc wystarczy tylko zaznaczyć Thread.CurrentThread. Widać to nawet w powyższym kodzie, w którym SetCurrentPrincipal zawsze ustawia Thread.CurrentPrincipal plus aktualizacje HttpContext (jeśli istnieje) są takie same; w żadnym momencie nie mają różnych wartości. (Innymi słowy, zawsze używaj Thread.CurrentThread.) –

+2

@Sly - Są takie same, z wyjątkiem sytuacji, gdy nie są. http://www.hanselman.com/blog/SystemThreadingThreadCurrentPrincipalVsSystemWebHttpContextCurrentUserOrWhyFormsAuthenticationCanBeSubtle.aspx –

0

Nie ma to związku z używaniem Moq do testowania jednostkowego tego, czego potrzebujesz.

Ogólnie rzecz biorąc, w pracy mamy warstwową architekturę, w której kod na warstwie prezentacji jest tak naprawdę tylko po to, aby uporządkować rzeczy wyświetlane w interfejsie użytkownika. Ten rodzaj kodu nie jest objęty testami jednostkowymi. Cała reszta logiki znajduje się w warstwie biznesowej, która nie musi być zależna od warstwy prezentacji (np. Odnośniki specyficzne dla UI, takie jak HttpContext), ponieważ interfejs użytkownika może być również aplikacją WinForm i niekoniecznie aplikacją internetową .

W ten sposób można uniknąć bałaganu w środowisku Mock, próbując symulować żądania HttpRequest itp., Chociaż często może to być konieczne.

3

Generalnie do testowania jednostek ASP.NET zamiast uzyskiwania dostępu do HttpContext.Current powinieneś mieć właściwość typu HttpContextBase, której wartość jest ustawiana przez wtrysk zależności (np. W odpowiedzi dostarczanej przez Womp).

Do testowania funkcji związanych z bezpieczeństwem zalecam używanie Thread.CurrentThread.Principal (zamiast HttpContext.Current.User). Korzystanie z Thread.CurrentThread ma tę zaletę, że można go używać również poza kontekstem sieciowym (i działa tak samo w kontekście sieciowym, ponieważ środowisko ASP.NET zawsze ustawia te same wartości).

Aby następnie przetestować Thread.CurrentThread.Principal Zwykle używam klasy zakres, który ustawia Thread.CurrentThread do wartości testu, a następnie resetuje na Zużyty:

using (new UserResetScope("LOONEYTUNES\BUGSBUNNY")) { 
    // Put test here -- CurrentThread.Principal is reset when PrincipalScope is disposed 
} 

ten dobrze pasuje do standardowego zabezpieczenia .NET component - gdzie komponent ma znany interfejs (IPrincipal) i lokalizację (Thread.CurrentThread.Principal) - i będzie działał z każdym kodem, który poprawnie używa/sprawdza wobec Thread.CurrentThread.Principal.

Baza klasa zakres byłoby coś jak poniżej (ustawić jako niezbędne do rzeczy jak role dodawanie):

class UserResetScope : IDisposable { 
    private IPrincipal originalUser; 
    public UserResetScope(string newUserName) { 
     originalUser = Thread.CurrentPrincipal; 
     var newUser = new GenericPrincipal(new GenericIdentity(newUserName), new string[0]); 
     Thread.CurrentPrincipal = newUser; 
    } 
    public IPrincipal OriginalUser { get { return this.originalUser; } } 
    public void Dispose() { 
     Dispose(true); 
     GC.SuppressFinalize(this); 
    } 
    protected virtual void Dispose(bool disposing) { 
     if (disposing) { 
      Thread.CurrentPrincipal = originalUser; 
     } 
    } 
} 

Inną alternatywą jest, zamiast przy użyciu standardowego położenia elementu zabezpieczenia, napisać swoją aplikację do korzystania wprowadzono szczegóły bezpieczeństwa, np dodaj właściwość ISecurityContext za pomocą metody GetCurrentUser() lub podobnej, a następnie używaj jej konsekwentnie w całej aplikacji - ale jeśli zamierzasz to zrobić w kontekście aplikacji sieciowej, możesz równie dobrze użyć gotowego kontekstu z wtryskiem , HttpContextBase.

1

Zapraszamy do obejrzenia tej http://haacked.com/archive/2007/06/19/unit-tests-web-code-without-a-web-server-using-httpsimulator.aspx

Korzystanie klasę httpSimulator, będzie można zrobić zdać HttpContext do handler

HttpSimulator sim = new HttpSimulator("/", @"C:\intepub\?") 
.SimulateRequest(new Uri("http://localhost:54331/FileHandler.ashx? 
ticket=" + myticket + "&fileName=" + path)); 

FileHandler fh = new FileHandler(); 
fh.ProcessRequest(HttpContext.Current); 

HttpSimulator realizować to, czego potrzebujemy, aby uzyskać instancję HttpContext. Więc nie musisz używać tutaj Moq.

1
[TestInitialize] 
public void TestInit() 
{ 
    HttpContext.Current = new HttpContext(new HttpRequest(null, "http://tempuri.org", null), new HttpResponse(null)); 
} 

Ponadto można Min jak poniżej

var controllerContext = new Mock<ControllerContext>(); 
     controllerContext.SetupGet(p => p.HttpContext.Session["User"]).Returns(TestGetUser); 
     controllerContext.SetupGet(p => p.HttpContext.Request.Url).Returns(new Uri("http://web1.ml.loc")); 
Powiązane problemy