Sunday, March 29, 2009

UpdateModel, FormCollection and unit test

I ran into some problems with a Action Controller method that accepted an FormCollection. Inside the method I use UpdateModel to get the values from the form into the model-class.

Here is a skeleton implementation of the method:

[UPDATED]
Some insightful comments by Steve made me do some changes to this code. I have now updated it. The tip had to do with if the ValueProvider should be set on the Controller in the test or in the actual Action-method. I finally opted for the latter and changed it into this (significant code in bold).

[AcceptVerbs(HttpVerbs.Post)]       

        public ActionResult Create(FormCollection form)       

        {           

            // Create ViewData           

            ProductForm viewData = new ProductForm(new Product(), productOwnerRepository.Find().ToList());

 

            try           

            {               

                UpdateModel(viewData, form.ToValueProvider());               

 

                // Get the productowner for the selected product-owner id

                viewData.Product.Owner = productOwnerRepository.GetById(viewData.SelectedOwnerID);               

 

                // add new product to the repository               

                productRepository.Add(viewData.Product);               

                productRepository.Save();               

 

                // Go back to the list               

                return RedirectToAction("Index");           

            }           

            catch

            {

                ModelState.AddRuleViolations(viewData.Product.GetRuleViolations());

                return View(viewData);

            }       

        }

This works great but when I call this from my unit test the the values of my form (created in the unit test) is lost. I was very puzzled by this until I found this post in the asp.net mvc forum.

So what that means is that you’ll, in the unit test case, need set the ValueProvider, for the controller you’re testing, to the test form collection you’re sending to the method. Otherwise the Action Controller method will check the Request-property and that will be empty in the test case. Not obvious maybe but understandable when you think about it.

[UPDATED]
Just to be sure – note that the Action-method is using an overloaded version of the UpdateModel that makes sure that the sent-in form is used. I haven’t used the recommendation from the link above since I think it’s less readable/understandable.
The main point are still valid though.

Here is a short sample that test the action method above.

        [TestMethod]

        public void createControllerActionCanTakeAFormCollectionWithProductDataAndAddItToTheRepository()

        {

            var numberOfProductsBefore = productRepository.Find().Count();

            var form = CreateProductTestFormCollection();

 

            

            var result = productController.Create(form);

 

            Assert.IsNotNull(result);

            Assert.IsInstanceOfType(result, typeof(RedirectToRouteResult));

            var redirectResult = (RedirectToRouteResult)result;

            Assert.AreEqual("Index", redirectResult.RouteValues["action"]);

 

            Assert.AreEqual(numberOfProductsBefore + 1, productRepository.Find().Count());

        }

 

        private static FormCollection CreateProductTestFormCollection()

        {

            FormCollection form = new FormCollection();

            form.Add("Product.Name", TESTFORM_NAME);

            form.Add("Product.Description", TESTFORM_DESCRIPTION);

            form.Add("SelectedOwnerID", TestData.TEST_PRODUCTOWNER_ID2.ToString());

 

            return form;

        }

6 comments:

Anonymous said...

I came across the same issue a while ago. I did the same thing as you at first but then changed it round to use one of the UpdateModel overloads and passing the value provider into the method in the controller itself.
UpdateModel(entity, formCollection.ToValueProvider());

Marcus said...

Hi Steve,

I thought of that but didn't go that way since the only reason for doing that is to get the tests to work.

So I think it's cleaner to have the Arrange-part of the test (that sets up the test-data) to instruct the controller which ValueProvider to use for this specific run.

But, as you said, there is a loads of overloads to UpdateModel and one of them can be used to get the case to work.

Thanks for you comment

Anonymous said...

Hi Marcus, yes, both ways work fine!

I thought that as the test was to prove that the controller can take a FormCollection (the FormCollection is a parameter for the method) it was cleaner to set it inside the controller method. Unless I'm mistaken, in your Create method the FormCollection that is passed in is never actually used, the values are 'magically' picked up by the UpdateModel method from the current ValueProvider. So I 'think' there are 2 'correct' ways to do it. First to pass the FormCollection in and pass it to one of the UpdateModel overloads. Second, NOT to pass the FormCollection in and set the ValueProvider in the unit test method. Passing the FormCollection into the Create method but not passing it to the UpdateModel method AND setting the ValueProvider in the unit test method it feels wrong somehow, but I could be missing something :)

Marcus said...

You are right! I didn't see that.

The form-collection variable is not used explicitly in the Create-method.

Hmmm - you are also right in that my code right now is mixing two options. I really don't like that...

Our discussion about "right" and "wrong" may actually be about readability. And in that sense your suggestion is more explicit and hence "better" I think.

If I find time I'll change the post based on your tip.

Thank you Steve!

Typemock said...

Disclaimer – I work at Typemock

I'm sure you know that using an isolation framework like Typemock or Rhinomocks can make unit testing this much easier – usually with less lines of code as well.

Marcus said...

Hello mr TypeMock (your name gave you away :))

I am well aware of Mocking Frameworks - my favorite is NMock.

But in this example and in the ASP.NET MVC framework, one of the great advantages is that you can do test without having to mock extensivly.

I still mock the dependencies for my Controller (done in the TestSetup-method), but that is done "manually" by implementing a Repository for testing.

Thanks for the tip, though.

Happy mocking!