开发者

How to prepare the controllers to use Session but be testable?

How to prepare the ASP.NET MVC Controllers to use Session and at the same time be testable, so in essence not use Session but rather use some Session abstraction? I am using Ninject so your examples could be based on that.

The problem is that the Session object is not available at all times in the controllers (like in ctor's) but I need to store something to the Session at application start (the global.asax.cs also does开发者_JAVA百科 not have access to the Session).


Just use a mock framework to create a mock HttpSessionStateBase and inject it into the controller context. With Rhino Mocks, this would be created using MockRepository.PartialMock<HttpSessionStateBase>() (see below). The controller, during the test, will then operate on the mock Session.

var mockRepository = new MockRepository();
var controller = new MyController();
var mockHttpContext = mockRepository.PartialMock<HttpContextBase>();
var mockSessionState = mockRepository.PartialMock<HttpSessionStateBase>();
SetupResult.For(mockHttpContext.Session).Return(mockSessionState);
// Initialize other parts of the mock HTTP context, request context etc
controller.ControllerContext = 
    new ControllerContext(
        new RequestContext(
            mockHttpContext, 
            new RouteData()
        ), 
        controller
    );


If you want your class to be testable, don't use non-testable (external) components there. When you try to mock them, you just work around the bad design. Re-design you controllers instead. A class shouldn't rely on external/global objects. That's one of the reasons why IoC is used.

You have two choices to separate implementation/infrastructure details from your controllers:

  1. Minor abstraction

    public interface ISession
    {
       string GetValue(string name);
       void SetValue(string name, string value);
    }
    
  2. Domain abstraction.

    public interface IStateData
    {
        bool IsPresent { get; }
        int MyDomainMeaningfulVariable { get; set; }
    }
    

In the latter case, the interface adds semantics over session - strongly typed, well named property. This is just as using NHibernate domain entities instead of sqlreader["DB_COLUMN_NAME"].

Then, of course, inject HTTP implementation of the interface (e.g. using HttpContext.Current) into controllers.

Model binders are also a good way to go, just as action filters are. They're not just for form data.


There are a couple of ways - either use a custom filter attribute to inject a session value into a controller action, or create a session object with an interface that can be mocked and inject it into the constructor of the controller.

Below is an example of the custom filter.

public class ProfileAttribute : ActionFilterAttribute
{
    public override void OnActionExecuting(ActionExecutingContext filterContext)
    {

        filterContext.ActionParameters["profileUsername"] = "some session value";

        base.OnActionExecuting(filterContext);
    }
}`

and a way to use it in a controller:

[ProfileAttribute]
public ActionResult Index(string profileUsername)
{
    return View(profileUsername);
}

Which one you choose probably depends on how much you rely on session values, but either way is relatively testable.


You can't store anything to Session at application start. Session = client initiated interaction. You don't have Sessions for all clients at application start.

Controller usually do not interact with session directly - it makes controllerd dependent on session. Instead controller methods (actions) accepts parameters which are automatically filled from the session by creating custom ModelBinder. Simple example:

public class MyDataModelBinder : IModelBinder
{
  private const string _key = "MyData";

  public object BindModel(ControllerContext context, ModelBindingContext bindingContext)
  {
    MyData data = (MyData)context.HttpContext.Session[_key];

    if (data == null)
    {
      data = new MyData();
      context.HttpContext.Session[_key] = data;
    }

    return data;
  }
}

You will than register your binder in Application_Start (Global.asax):

ModelBinders.Binders.Add(typeof(Mydata), new MyDataModelBinder());

And you define your action like:

public ActionResult MyAction(MyData data)
{ ... }

As you can see the controller is in no way dependent on Session and it is fully testable.

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜