Thomas Bandt

Über mich | Kontakt | Archiv

TDD: HttpContext mocken

Wer eine Web-Applikation oder Website mit ASP.NET test-getrieben entwickelt wird früher (WebForms) oder später (MVC) an den Punkt kommen, an dem er im Controller auf den aktuellen HttpContext zugreifen muss.

Ein klassischer Fall ist hier z.B. der Zugriff auf den QueryString-Parameter "ReturnUrl", der von ASP.NET bei Verwendung von FormsAuthentication gesetzt wird, wenn ein nicht authentifizierter Zugriff auf eine Seite zum Login weitergeleitet wird, um dann über diesen Parameter nach erfolgreichem Login auf das eigentliche Ziel zurückzukehren.

Nun wäre ein klassischer Testfall, dass man vorgibt, nicht auf dieses Ziel weiterzuleiten, wenn es z.B. eine externe URL ist - Phishing lässt grüßen. Um das zu ermöglichen, muss man aber im Test den Inhalt von Request.QueryString vorgeben können.

Die Voraussetzung dafür ist, dass man diese ziemlich fiese Abhängigkeit zur Core-Infrastruktur des Frameworks auflöst und den HttpContext in den Controller injiziert:

   1:  public class LoginController : Controller
   2:  {
   3:   
   4:      public LoginController(HttpContextBase httpContext)
   5:      {
   6:          HttpContext = httpContext;
   7:      }
   8:   
   9:      private new readonly HttpContextBase HttpContext;
  10:   
  11:      [HttpPost]
  12:      public ActionResult Index(LoginViewModel model)
  13:      {
  14:   
  15:          // ...
  16:   
  17:          string returnUrl = HttpContext.Request.QueryString["ReturnUrl"];
  18:          if (!string.IsNullOrWhiteSpace(returnUrl) && returnUrl.StartsWith("/"))
  19:              return Redirect(returnUrl);
  20:   
  21:          return RedirectToAction("index", "home");
  22:   
  23:      }
  24:   
  25:  }

Das kann man nun nutzen, um in den Tests dafür die Daten beliebig zu faken:

   1:  [TestFixture]
   2:  [Category("HttpContextUnitTestSample.Controllers.LoginController")]
   3:  public class Wenn_beim_Login_eine_ReturnUrl_angegeben_ist : ConcernOf<LoginController>
   4:  {
   5:   
   6:      protected HttpContextBase HttpContext;
   7:      protected HttpRequestBase HttpRequest;
   8:   
   9:      public override void Setup()
  10:      {
  11:   
  12:          HttpContext = MockRepository.DynamicMock<HttpContextBase>();
  13:          HttpRequest = MockRepository.DynamicMock<HttpRequestBase>();
  14:              
  15:          Sut = new LoginController(HttpContext);
  16:   
  17:          using (MockRepository.Record())
  18:          {
  19:              HttpContext.Expect(c => c.Request).Return(HttpRequest).Repeat.Any();
  20:          }
  21:   
  22:      }
  23:   
  24:      [Test]
  25:      public void Wird_zu_dieser_weitergeleitet_sofern_es_eine_interne_Url_ist()
  26:      {
  27:   
  28:          HttpRequest.BackToRecord(BackToRecordOptions.Expectations);
  29:          using (MockRepository.Record())
  30:          {
  31:              HttpRequest.Expect(r => r.QueryString["ReturnUrl"]).Return("/usermanagement");
  32:          }
  33:   
  34:          var result = (RedirectResult)Sut.Index(
  35:              new LoginViewModel { UserName = "valid", Password = "password" });
  36:          Assert.AreEqual("/usermanagement", result.Url);
  37:   
  38:      }
  39:   
  40:      [Test]
  41:      public void Wird_zur_Startseite_weitergeleitet_wenn_sie_nicht_mit_einem_Slash_beginnt()
  42:      {
  43:   
  44:          HttpRequest.BackToRecord(BackToRecordOptions.Expectations);
  45:          using (MockRepository.Record())
  46:          {
  47:              HttpRequest.Expect(r => r.QueryString["ReturnUrl"]).Return("http://phishingurl.com");
  48:          }
  49:   
  50:          var result = (RedirectToRouteResult)Sut.Index(
  51:              new LoginViewModel { UserName = "valid", Password = "password" });
  52:          Assert.AreEqual("home", result.RouteValues["controller"]);
  53:   
  54:      }
  55:   
  56:  }

Ich arbeite hier wie üblich mit Rhino.Mocks. Auf diese Weise lässt sich relativ einfach und völlig frei bestimmen, was an Daten über den HttpContext bereitgestellt wird.

Damit das auch in der echten Anwendung funktioniert, muss das IoC-Tool der Wahl noch so konfiguriert werden, dass bei der Ausführung des Controllers "in der realen Welt" der tatsächliche aktuelle HttpContext injiziert wird. Mein bevorzugtes Werkzeug hierfür ist StructureMap, womit das so aussähe:

   1:  public class Bootstrapper
   2:  {
   3:   
   4:      public static void Bootstrap()
   5:      {
   6:          ObjectFactory.Initialize(c =>
   7:          {
   8:              c.AddRegistry(new FrameworkInfrastructureRegistry());
   9:          });
  10:      }
  11:   
  12:      private class FrameworkInfrastructureRegistry : Registry
  13:      {
  14:          public FrameworkInfrastructureRegistry()
  15:          {
  16:              For<HttpContextBase>().Use(() => new HttpContextWrapper(HttpContext.Current));
  17:          }
  18:      }
  19:   
  20:  }

Der Bootstrapper wird dann einfach in Application_Start() in der Global.asax aufgerufen. That's it.

Kommentare

  1. Thomas goes .NET schrieb am Freitag, 14. Mai 2010 14:45:00 Uhr:

    Als ich vorhin am Artikel über das Mocken des HttpContext schrieb, wollte ich eigentlich eine Demo-Anwendung dazu packen. Als ich mit ihr fertig war, fiel mir auf, dass ich damit leicht über das Ziel hinaus geschossen war. Denn in der App ist viel me ...


« Zurück  |  Weiter »