Zugriff auf die Session in ASP.NET MVC Controllern
Wednesday, May 7th, 2008Bei Verwendung des ASP.NET MVC Frameworks besteht meist die Notwendigkeit im Controller auf die HTTP Session zuzugreifen um dort den Zustand abzulegen. Eine naive Implementierung sieht z.B. so aus:
public RenderViewResult Warenkorb() { const string key = "warenkorb"; if (HttpContext.Session[key] == null) { HttpContext.Session.Add(key, ""); } string warenkorb = (string)HttpContext.Session[key]; ViewData[key] = warenkorb; return RenderView("Warenkorb", ViewData); }
Dass diese Implementierung nicht die beste ist erkennt man (spätestens) wenn man versucht die Methode Warenkorb zu testen:
[TestFixture] public class HomeControllerTests { private HomeController homeController; [SetUp] public void Setup() { homeController = new HomeController(); } [Test] public void Action_Warenkorb() { RenderViewResult renderViewResult = homeController.Warenkorb(); Assert.That(renderViewResult.ViewName, Is.EqualTo("Warenkorb")); } }
Ergebnis bei Testausführung: System.NullReferenceException beim Zugriff auf den HttpContext. Dieser wird vom ASP.NET MVC Framework zur Laufzeit bereitgestellt, nicht jedoch wenn wir nur den Controller per Konstruktor instanziieren.
Nun kann man natürlich hingehen und mit Hilfe eines Mock Frameworks die Infrastruktur des Controllers bereitstellen. Die Tests sind dann jedoch sehr aufwendig, schlecht lesbar und dazu noch sehr zerbrechlich:
[SetUp] public void Setup() { homeController = new HomeController(); mocks = new MockRepository(); HttpContextBase httpContext = mocks.DynamicMock<HttpContextBase>(); session = mocks.DynamicMock<HttpSessionStateBase>(); SetupResult.For(httpContext.Session).Return(session); ControllerContext controllerContext = new ControllerContext( httpContext, new RouteData(), homeController); homeController.ControllerContext = controllerContext; } [Test] public void Action_Warenkorb() { using (mocks.Record()) { Expect.Call(session["warenkorb"]).Return("5 Äpfel"); } using (mocks.Playback()) { RenderViewResult renderViewResult = homeController.Warenkorb(); Assert.That(renderViewResult.ViewName, Is.EqualTo("Warenkorb")); } }
Viel eleganter lässt sich das Problem lösen in dem wir dem Controller im Konstruktor ein IStateProvider Objekt übergeben mit dessen Hilfe der Controller auf die Session zugreift. Der State Provider lässt sich im Test leicht durch ein Mock Objekt ersetzen:
public interface IStateProvider<T> { T GetState(Controller controller); void SetState(Controller controller, T state); }
public class StateProvider<T> : IStateProvider<T> where T : new() { private const string StateKey = "state"; public T GetState(Controller controller) { if (controller.HttpContext.Session[StateKey] == null) { SetState(controller, new T()); } return (T)controller.HttpContext.Session[StateKey]; } public void SetState(Controller controller, T state) { if (controller.HttpContext.Session[StateKey] == null) { controller.HttpContext.Session.Add(StateKey, state); } else { controller.HttpContext.Session[StateKey] = state; } } }
[SetUp] public void Setup() { mocks = new MockRepository(); stateProvider = mocks.DynamicMock<IStateProvider<string>>(); homeController = new HomeController(stateProvider); } [Test] public void Action_Warenkorb() { using (mocks.Record()) { Expect.Call(stateProvider.GetState(homeController)).Return("5 Äpfel"); } using (mocks.Playback()) { RenderViewResult renderViewResult = homeController.Warenkorb(); Assert.That(renderViewResult.ViewName, Is.EqualTo("Warenkorb")); } }
container.AddComponentWithLifestyle("stateProvider", typeof(IStateProvider<>), typeof(StateProvider<>), LifestyleType.Transient);


