使用ModelBinder綁定IPrincipal (User)簡化ASP.NET MVC單元測試

來源:互聯網
上載者:User

I am working on some code like this:

?
123456789101112131415 [Authorize]public ActionResult Edit(int id) {     Dinner dinner = dinnerRepository.FindDinner(id);     if (dinner.HostedBy != User.Identity.Name)        return View("InvalidOwner");     var viewModel = new DinnerFormViewModel {        Dinner = dinner,        Countries = new SelectList(PhoneValidator.Countries, dinner.Country)    };     return View(viewModel);}

It's pretty straight forward, but this Controller knows too much. It's reaching into implicit parameters. The id was passed in, but the User is actually a property of the Controller base class and ultimately requires an HttpContext. Having this method "know" about the User object, and worse yet, having the User object go reaching into HttpContext.Current makes this hard to test.

I'd like to have the convenience of passing in the User (actually an IPrincipal interface) when I want to test, but when I'm running the app, I'd like to have the IPrincipal get passed into my method automatically. Enter the Model Binder. I need to teach ASP.NET MVC what to do when it sees a type as a parameter.

This quickie model binder is now responsible for one thing - it knows how to reach down into the HttpContext and get the current User (IPrincipal). It has one single responsibility.

?
1234567891011121314 public class IPrincipalModelBinder : IModelBinder{    public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)    {        if (controllerContext == null) {            throw new ArgumentNullException("controllerContext");        }        if (bindingContext == null) {            throw new ArgumentNullException("bindingContext");        }        IPrincipal p = controllerContext.HttpContext.User;        return p;    }}

Now I can release the Controller from the emotional baggage of knowing too much about the User object. It can just have that passed in automatically by the framework. I just need to register the binder to tell folks about it. I can either do it on a one-off basis and put an attribute on this one method parameter:

?
1234 public ActionResult Edit(int id,                                                 [ModelBinder(typeof(IPrincipalModelBinder))]                                                 IPrincipal user){...}

But even better, I can just tell the whole application once in the global.asax:

?
1234 void Application_Start() {    RegisterRoutes(RouteTable.Routes); //unrelated, don't sweat this line.    ModelBinders.Binders[typeof(IPrincipal)] = new IPrincipalModelBinder();}

Now that ASP.NET MVC knows what to do when it see an IPrincipal as a method parameter, my method gets nicer.

?
123456789101112131415 [Authorize]public ActionResult Edit(int id, IPrincipal user) {     Dinner dinner = dinnerRepository.FindDinner(id);     if (dinner.HostedBy != user.Identity.Name)        return View("InvalidOwner");     var viewModel = new DinnerFormViewModel {        Dinner = dinner,        Countries = new SelectList(PhoneValidator.Countries, dinner.Country)    };     return View(viewModel);}

Now I can test my controller more easily by passing in fake users. No need for mocking in this case!

?
1234567891011121314 [TestMethod]public void EditAllowsUsersToEditDinnersTheyOwn(){    // Arrange    DinnersController controller = new DinnersController(new TestDinnerRespository());     // Act    IPrincipal FakeUser = new GenericPrincipal(new GenericIdentity("Scott","Forms"),null);    ViewResult result = controller.Edit(4, FakeUser) as ViewResult;     // Yada yada yada assert etc etc etc    Assert.IsTrue(result.ViewName != "InvalidOwner");}

Fun stuff.

UPDATE: Phil had an interesting idea. He said, why not make method overloads, one for testing and one for without. I can see how this might be controversial, but it's very pragmatic.

?
12345 [Authorize]public ActionResult Edit(int id){    return Edit(id, User); //This one uses HttpContext}

You'd use this one as before at runtime, and call the overload that takes the IPrincipal explicitly for testing.

Yes, I realize I could use an IoC container for this also.

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.