views:

906

answers:

2

I have been working my way through Scott Guthrie's excellent post on ASP.NET MVC Beta 1. In it he shows the improvements made to the UpdateModel method and how they improve unit testing. I have recreated a similar project however anytime I run a UnitTest that contains a call to UpdateModel I receive an ArgumentNullException naming the controllerContext parameter.

Here's the relevant bits, starting with my model:

public class Country {
  public Int32 ID { get; set; }
  public String Name { get; set; }
  public String Iso3166 { get; set; }
}

The controller action:

[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Edit(Int32 id, FormCollection form)
{
  using ( ModelBindingDataContext db = new ModelBindingDataContext() ) {
    Country country = db.Countries.Where(c => c.CountryID == id).SingleOrDefault();

    try {
      UpdateModel(country, form);

      db.SubmitChanges();

      return RedirectToAction("Index");
    }
    catch {
      return View(country);
    }
  }
}

And finally my unit test that's failing:

[TestMethod]
public void Edit()
{
  CountryController controller = new CountryController();
  FormCollection form = new FormCollection();
  form.Add("Name", "Canada");
  form.Add("Iso3166", "CA");

  var result = controller.Edit(2 /*Canada*/, form) as RedirectToRouteResult;

  Assert.IsNotNull(result, "Expected to be redirected on successful POST.");
  Assert.AreEqual("Show", result.RouteName, "Expected to redirect to the View action.");
}

ArgumentNullException is thrown by the call to UpdateModel with the message "Value cannot be null. Parameter name: controllerContext". I'm assuming that somewhere the UpdateModel requires the System.Web.Mvc.ControllerContext which isn't present during execution of the test.

I'm also assuming that I'm doing something wrong somewhere and just need to pointed in the right direction.

Help Please!

+2  A: 

I don't think it can be done since TryUpdateModel, which UpdateModel uses, references the ControllerContext which is null when invoked from a unit test. I use RhinoMocks to mock or stub the various components needed by the controller.

var routeData = new RouteData();
var httpContext = MockRepository.GenerateStub<HttpContextBase>();
FormCollection formParameters = new FormCollection();

EventController controller = new EventController();
ControllerContext controllerContext = 
    MockRepository.GenerateStub<ControllerContext>( httpContext,
                                                    routeData,
                                                    controller );
controller.ControllerContext = controllerContext;

ViewResult result = controller.Create( formParameters ) as ViewResult;

Assert.AreEqual( "Event", result.Values["controller"] );
Assert.AreEqual( "Show", result.Values["action"] );
Assert.AreEqual( 0, result.Values["id"] );

Here's the relevant bit from the Controller.cs source on www.codeplex.com/aspnet:

protected internal bool TryUpdateModel<TModel>( ... ) where TModel : class
{

     ....

    ModelBindingContext bindingContext =
           new ModelBindingContext( ControllerContext,
                                    valueProvider,
                                    typeof(TModel),
                                    prefix,
                                    () => model,
                                    ModelState,
                                    propertyFilter );

     ...
}
tvanfosson
I agree, I could solve this by mocking, but that goes explicitly against what Scott says in his post in reference to the UpdateModel examples: "We did not have to mock anything to unit test both of the above form submission scenarios."
Hellfire
I looked at the source for TryUpdateModel (which UpdateModel uses) and it definitely uses the ControllerContext. I've updated my response with the relavant source code bit.
tvanfosson
@Hellfire, I saw a comment on the blog post that indicated that at least one other person was having the same error. Could be that ModelBindingContext changed before Beta 1 got out the door.
tvanfosson
You're correct, but this is a bug. ISTR the team plans to fix it.
Craig Stuntz
@Tim, Thanks. I read through the source too and I hope you're right. It'd be a real shame if it shipped like this.
Hellfire
@Craig, Do you have any sources (blogs, articles, etc.) that confirm this? I guess I can follow TV's suggestion and mock it but obviously that's not ideal.
Hellfire
I'm accepting this answer and hoping that the dependency TryUpdateModel has on the ControllerContext goes away in the future.
Hellfire
A: 

Or you can create form data proxy, like

public class CountryEdit {
  public String Name { get; set; }
  public String Iso3166 { get; set; }
}
  • Plus. Easy create unit tests
  • Plus. Define white list of fields update from post
  • Plus. Easy setup validation rules, easy test it.
  • Minus. You should move date from proxy to you model

So Controller.Action should look, like

public ActionResult Edit(Int32 id, CountryEdit input)
{
  var Country = input.ToDb();
  // Continue your code
}
SergXIIIth