Category Archives: 10390

Experiences upgrading an MVC 1 application to MVC 3

I have recently had to do some work on a MVC 1 application and thought it sensible to bring it up to MVC 3, you don’t want to be left behind if you can avoid it. This was a simple data capture application written in MVC1 in Visual Studio 2008 and never needed to be touched since. A user fills in a form, the controller then takes the form contents and stores it. The key point to note here is that it was using the Controller.UpdateModel<TModel> Method (TModel, IValueProvider) method, so most of the controller actions look like

[AcceptVerbs(HttpVerbs.Post)]
       public ActionResult PostDataToApplication(FormCollection form)
       {
           NewApplication data = new NewApplication();
           try
           {
               UpdateModel(data, form.ToValueProvider());
              
               // process data object
               bool success = DoSomething(data);

               if (success)
               {
                   return RedirectToAction("FormSuccess", "Home");
               }
               else
               {
                   return RedirectToAction("FormUnsuccessful", "Home");
               }
           }
           catch (InvalidOperationException)
           {
               return View();
           }
       }

This worked fine on MVC 1 on VS2008, but I wanted to move it onto MVC3 on VS2012 if possible; with a little changes as possible as this is a small web site that has very few changes so not worth a major investment in time to keep updated to current frameworks. So these were steps I took and gotcha’s I found

The upgrade itself

First I opened the VS2008 solution in VS2010 and it automatically upgraded to MVC2, a good start!

I then used the MVC2 to MVC3 tool on Codeplex, this initially failed and it took me a while to spot that you can only use this tool if your MVC2 application targets .NET 4. Once I changed the MVC2 to target .NET 4 as opposed to 3.5 this upgrade tool worked fine.

I could now load my MVC3 application in either VS2010 or VS2012.

Using the Web Site

At this point I though I had better test it, and instantly saw a problem. Pages that did not submit data worked fine, but submitting a data capture forms failed with Null Exception errors. Turns out the problem was a change in default behaviour of the models between MVC releases. On MVC1 empty fields on the form were passed as empty strings, with MVC 2(?) and later they are passed as nulls.

Luckily the fix was simple. Previously my model had been

public class SomeModel: IDataErrorInfo
{
     public string AgentNumber { get; set; }
     …..
}

I needed to add a  [DisplayFormat(ConvertEmptyStringToNull = false)] attribute on  each string property to get back to the previous behaviour my controller expected

public class SomeModel: IDataErrorInfo
{
    [DisplayFormat(ConvertEmptyStringToNull = false)]
     public string AgentNumber { get; set; }
     …..
}

Now my web site ran as I had expected.

Unit Tests

I had previously noticed my unit tests were failing. I had expected the change to the model would fix this too, but it did not. On the web there a good many posts as to how unit testing of MVC2 and later fails unless you mock out the controller.context. You see errors in the form

Test method Website.Tests.Controllers.HomeControllerTest.DetailsValidation_AlphaInValidAccountID_ErrorMessage threw exception:
System.ArgumentNullException: Value cannot be null.
Parameter name: controllerContext
Result StackTrace:   
at System.Web.Mvc.ModelValidator..ctor(ModelMetadata metadata, ControllerContext controllerContext)
   at System.Web.Mvc.ModelValidator.CompositeModelValidator..ctor(ModelMetadata metadata, ControllerContext controllerContext)
   at System.Web.Mvc.ModelValidator.GetModelValidator(ModelMetadata metadata, ControllerContext context)
   at System.Web.Mvc.DefaultModelBinder.OnModelUpdated(ControllerContext controllerContext, ModelBindingContext bindingContext)
   at System.Web.Mvc.DefaultModelBinder.BindComplexElementalModel(ControllerContext controllerContext, ModelBindingContext bindingContext, Object model)
   at System.Web.Mvc.DefaultModelBinder.BindComplexModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
   at System.Web.Mvc.DefaultModelBinder.BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
   at System.Web.Mvc.Controller.TryUpdateModel[TModel](TModel model, String prefix, String[] includeProperties, String[] excludeProperties, IValueProvider valueProvider)
   at System.Web.Mvc.Controller.UpdateModel[TModel](TModel model, String prefix, String[] includeProperties, String[] excludeProperties, IValueProvider valueProvider)
   at System.Web.Mvc.Controller.UpdateModel[TModel](TModel model, IValueProvider valueProvider)
   at Website.Controllers.HomeController.Details(FormCollection form)

The fix is to not just new up a controller in your unit tests like this

HomeController controller = new HomeController();

But to have a helper method to mock it all out (which is created for MVC associated test projects for you, so it is easy)

private static HomeController GetHomeController()
{
   IFormsAuthentication formsAuth = new MockFormsAuthenticationService();     
   MembershipProvider membershipProvider = new MockMembershipProvider();
   RoleProvider roleProvider = new MockRoleProvider();

   AccountMembershipService membershipService = new AccountMembershipService(membershipProvider, roleProvider);
   HomeController controller = new HomeController(formsAuth, membershipService);
   MockHttpContext mockHttpContext = new MockHttpContext();

   ControllerContext controllerContext = new ControllerContext(mockHttpContext, new RouteData(), controller);
   controller.ControllerContext = controllerContext;
   return controller;
}

However, this problem with a missing context was not my problem, I was already doing this. The error my test runner was showing did not mention the context, rather binding errors.

Test method CollectorWebsite.Tests.Controllers.HomeControllerTest.CardValidation_AlphaInValidAccountID_ErrorMessage threw exception:
System.NullReferenceException: Object reference not set to an instance of an object.
Result StackTrace:   
at CollectorWebsite.Models.CardRecovery.get_Item(String columnName) 
   at System.Web.Mvc.DataErrorInfoModelValidatorProvider.DataErrorInfoPropertyModelValidator.Validate(Object container)
   at System.Web.Mvc.ModelValidator.CompositeModelValidator.<Validate>d__5.MoveNext()
   at System.Web.Mvc.DefaultModelBinder.OnModelUpdated(ControllerContext controllerContext, ModelBindingContext bindingContext)
   at System.Web.Mvc.DefaultModelBinder.BindComplexElementalModel(ControllerContext controllerContext, ModelBindingContext bindingContext, Object model)
   at System.Web.Mvc.DefaultModelBinder.BindComplexModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
   at System.Web.Mvc.DefaultModelBinder.BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
   at System.Web.Mvc.Controller.TryUpdateModel[TModel](TModel model, String prefix, String[] includeProperties, String[] excludeProperties, IValueProvider valueProvider)
   at System.Web.Mvc.Controller.UpdateModel[TModel](TModel model, String prefix, String[] includeProperties, String[] excludeProperties, IValueProvider valueProvider)
   at System.Web.Mvc.Controller.UpdateModel[TModel](TModel model, IValueProvider valueProvider)
   at CollectorWebsite.Controllers.HomeController.CardRecovery(FormCollection form)

I got stuck here for a good while………

Then it occurred to me if the behaviour has changed such that on the web site I see nulls when I expect empty strings, I bet the same is happening in unit tests. It is trying to iterate though what was a collection of strings and is now at best a collection of nulls or just an empty collection. The bind failed as it could not match the form to the data.

The fix was to make sure in my unit tests I passed in a FormCollection that had all the expected fields (with suitable empty values e.g string.empty). This meant my unit tests changed from

[TestMethod, Isolated]
public void ApplicationValidation_CurrentPostcodeNumbersOnly_ErrorMessage()
{

           // Arrange
           HomeController controller = GetHomeController();
      
    FormCollection form = new FormCollection();
           form.Add("CurrentPostcode", "12345");

           // Act
           ViewResult result = controller.Application(form) as ViewResult;

           // Assert
           Assert.IsNotNull(result);
           Assert.AreEqual("Please provide a valid postcode", result.ViewData.ModelState["CurrentPostcode"].Errors[0].ErrorMessage);

       }

To

[TestMethod, Isolated]
public void ApplicationValidation_CurrentPostcodeNumbersOnly_ErrorMessage()
{

           // Arrange
           HomeController controller = GetHomeController();
          
FormCollection form = GetEmptyApplicationFormCollection();
           form.Set("CurrentPostcode", "12345");

           // Act
           ViewResult result = controller.Application(form) as ViewResult;

           // Assert
           Assert.IsNotNull(result);
           Assert.AreEqual("Please provide a valid postcode", result.ViewData.ModelState["CurrentPostcode"].Errors[0].ErrorMessage);

       }

where the GetEmptyApplicationFormCollection() helper method just creates a FormCollection with all the forms fields.

Once this was done my unit test passed.

Summary

So I now have an MVC3 application that works and passes unit tests. You could argue I should do more work so it does not need these special fixes, but it meets my needs for now.

Testing access attributes on the Microsoft MVC framework

The MVC framework provides an excellent way to create a testable web site. In fact when you create a new MVC project you are given the option to create an associated test project that contains MSTEST unit tests for all the sample methods in the MVC Controller class; which you can add to as you go along.

Recently whilst working on an MVC project I noticed that the one area that this model does not let you test is that of access control. MVC uses attributes on Controller methods to limit who can access what e.g.

[Authorize]
public ActionResult ChangePassword()
{

ViewData["PasswordLength"] = MembershipService.MinPasswordLength;

return View();
}


These attributes are used by the MVC routing engine to decide if a method can be called or not. The problem is these attributes are not honoured by the unit tests as they call the controller methods directly not via the routing engine.



This raised a concern for me, if I am setting access controller via attributes how can I make sure I set the right attribute on the right methods? This is important for regression testing. This issue has also been discussed on StackOverflow. An alternative is to not use this attribute security model, but to make the security checks within the controller methods programmatically. For some this might be the correct solution, but did not see right for me. If you are going to use a framework, try to use it as it was intended.



I therefore needed a way to honour the attributes whilst testing. One option would be to write code on the unit tests to check the attributes, but this was more reflection than I wanted to do at this time. So I thought of using Ivonna the Typemock add-in. This allows you to load a web page and process it via a mocked web delivery framework.



Now this plan seemed simple but as soon as I started I found it was more complex than expected. I hit some problem as I will detail as I go along. I must say thanks to Artem Smirnov who wrote Ivonna for all his help in sorting out the issues I was having, both in my usage/understanding and fixing bugs. I could not have written this post without his help.



Preparation- My Assumptions



So for this post lets assume the following



  • You create a new MVC project so you have the basic sample MVC web site.
  • You allow Visual Studio to also create a new Test Project for the MVC project
  • You have Typemock Isolator 5.3.0
  • You have Ivonna 1.2.6 (maybe need Artem’s “experimental support for MVC" special release, see comments below)


GOTT’A 1: If you are writing tests using MSTEST with Ivonna you have to set the Test Project output directory to the bin directory of the MVC project (via Test Project’s properties, build tab)  e.g. ..\MvcApplication1\bin. This is needed so that Ivonna can find the page classes to load.



image .



Submitting a Form





The basic way MVC works is that forms get POSTed to the controller. So this was where I started, I think the comments in the code explain what is being done



[TestMethod, RunOnWeb(true)]
public void LogOn_FormContainingValidLoggedDetails_RedirectedToChangePasswordView()
{

// Arrange
// Fake out the membership service using Typemock, allow call through to the original
// class so that we don't need to fake out all the methods that will be called
var fakeMembershipService = Isolate.Fake.Instance<AccountMembershipService>(Members.CallOriginal);
// set that the Validate method will return true with the correct UID and password
Isolate.WhenCalled(() => fakeMembershipService.ValidateUser("testid", "goodpass")).WillReturn(true);

// Intercept the next call to the AccountController and slip in the faked service
Isolate.Swap.NextInstance<AccountMembershipService>().With(fakeMembershipService);

// Create the Ivonna test session
var session = new TestSession();

// Create the POST request, setting the AutoRedirect flag so that it does not
// actually do the redirect at the end of processing. We just check where it would
// redirect if we let it, this avoids an extra round trip
WebRequest request = new WebRequest(@"/Account/LogOn", "POST", null, null) { AutoRedirect = false };

// Fill in the form values with all the fields that would be sent on the Logon view submission
request.FormValues.Add("username", "testid");
request.FormValues.Add("password", "goodPass");
request.FormValues.Add("rememberMe", false.ToString());
request.FormValues.Add("returnUrl", @"/Account/ChangePassword");

// Act
// Process the request
WebResponse response = session.ProcessRequest(request);

// Assert
// Check that we have been redirected to the correct page
Assert.AreEqual(@"/Account/ChangePassword", response.RedirectLocation);
}



GOTT’A 2: If you are using the current shipping version of Ivonna (1.2.6) this test will fail. There is a problem with form handling code so you need Artem’s “experimental support for MVC" release this addresses a problem with the form handling code. However as you will see from my comments below this problem might not as critical as I first thought.



This test is all well and good, but is it useful? All it proves is that Microsoft wrote their logon code correctly. They provide unit tests for this in the MVC source if you are interested. In my opinion if you are using a framework like you need to take it on trust and assume it’s core functions are tested prior to its publication (OK there will be bugs, but as a general point I think this holds true)



So I would say it is good you can do this test, but in practice I don’t think I would bother. If I want to test the functionality of methods in my controller class I should just use standard unit tests as in the MVC samples. I should test the functionality from separately from the security.



Checking for page differences between an anonymous user and an authenticated one



What I do want to test that a page is rendering correctly depending if I am logged in or not. The following two tests show how to do this with the home page of the default MVC sample.



I would draw your attention to how ‘clean’ the test is. Ivonna (and hence Typemock) is doing all the heavy lifting behind the scenes.



[TestMethod, RunOnWeb]
public void Home_IsNotLoggedOn_SeeLogonButton()
{
// Arrange
// create the Ivonna test session
var session = new TestSession();
// create the request for the page we want
WebRequest request = new WebRequest(@"/");
// set no user

// Act
WebResponse response = session.ProcessRequest(request);

// Assert
Assert.AreEqual(@"/", response.Url);
// we can check some html items on the form
Assert.IsTrue(response.BodyAsString.Contains("<h2>Welcome to ASP.NET MVC!</h2>"));
Assert.IsTrue(response.BodyAsString.Contains("[ <a href=\"/Account/LogOn\">Log On</a> ]"));
}




[TestMethod, RunOnWeb]
public void Home_IsLoggedOn_SeeLogOffButton()
{
// Arrange
// create the Ivonna test session
var session = new TestSession();
// create the request for the page we want
WebRequest request = new WebRequest(@"/");
// Pass in a user and the frame does the rest
request.User = new System.Security.Principal.GenericPrincipal(new System.Security.Principal.GenericIdentity("testid"), null);

// Act
WebResponse response = session.ProcessRequest(request);

// Assert
Assert.AreEqual(@"/", response.Url);
// we can check some html items on the form
Assert.IsTrue(response.BodyAsString.Contains("<h2>Welcome to ASP.NET MVC!</h2>"));
Assert.IsTrue(response.BodyAsString.Contains("Welcome <b>testid</b>!"));
Assert.IsTrue(response.BodyAsString.Contains("[ <a href=\"/Account/LogOff\">Log Off</a> ]"));
}



GOTT’A 3: When using classic ASP.NET Ivonna treats the returned page as an object and you have a collection of extension methods to help navigate the page checking control values etc. As MVC just returns HTML to the browser at this time you have to check for values by string matching in the response.BodyAsString (or you could use some xPath or Regular BLOCKED EXPRESSION



TIP: As I am sure you will be looking at HTML from response.BodyAsString property in the debugger at some point, using the HTML visualizer in the Visual Studio Auto/Local Window is a great help. Select the HTML visualizer (as opposed to the default string one) by using the drop down selector in the window or tool trip debug prompt, and you can see the page as if in a browser for a quick manual visual test.



Checking who can see a page



Where I started with this project was wanting to test page access i.e. can user A get to Page B? We are now in a position to achieve this. The following two tests check that an authenticated user can reach a secured page, and that a non authenticated one is redirected to the logon page. Again note the clean easy to read syntax.



[TestMethod, RunOnWeb]
public void ShowChangePassword_NotAuthenticated_RedirectToLogonView()
{
// Arrange
var session = new TestSession();
var request = new WebRequest(@"/Account/ChangePassword");
// we set no value for the request.User property

// Act
var response = session.ProcessRequest(request);

// Assert
// redirected to the logon page
Assert.AreEqual(@"Account/LogOn?ReturnUrl=%2fAccount%2fChangePassword", response.Url);
}



[TestMethod, RunOnWeb]
public void ShowChangePassword_Authenticated_ShowChangePasswordView()
{
// Arrange
var session = new TestSession();
var request = new WebRequest(@"/Account/ChangePassword");
// just pass in a user, using the fact that Ivonna has a built-in authentication faking
request.User = new System.Security.Principal.GenericPrincipal(new System.Security.Principal.GenericIdentity("testid"), null);

// Act
var response = session.ProcessRequest(request);

// Assert
Assert.AreEqual(@"/Account/ChangePassword", response.Url);
// we can also check the page content, but probably don't need to in this case
Assert.IsTrue(response.BodyAsString.Contains("Use the form below to change your password."));
Assert.IsTrue(response.BodyAsString.Contains("Welcome <b>testid</b>!"));
Assert.IsTrue(response.BodyAsString.Contains("[ <a href=\"/Account/LogOff\">Log Off</a> ]"));

}



The above tests assume that the ChangePassword page is just protected with a simple [Authorize] attribute, so a user is authenticated or not. However, it is easy to modify the test so that it handles roles. If the same page was protected with the attribute [Authorize(Roles= "Staff")], the test simply becomes



[TestMethod, RunOnWeb]
public void ShowStaffOnlyChangePassword_Authenticated_ShowChangePasswordView()
{
// Arrange
var session = new TestSession();
var request = new WebRequest(@"/Account/ChangePassword");
// just pass in a user, using the fact that Ivonna has a built-in authentication faking
request.User = new System.Security.Principal.GenericPrincipal(new System.Security.Principal.GenericIdentity("testid") , new string[] {"Staff"});

// Act
var response = session.ProcessRequest(request);

// Assert
Assert.AreEqual(@"/Account/ChangePassword", response.Url);
// we can also check the page content, but probably don't need to in this case
Assert.IsTrue(response.BodyAsString.Contains("Use the form below to change your password."));
Assert.IsTrue(response.BodyAsString.Contains("Welcome <b>testid</b>!"));
Assert.IsTrue(response.BodyAsString.Contains("[ <a href=\"/Account/LogOff\">Log Off</a> ]"));

}



Should I use this way to test MVC?



If you are worried that developers are not applying (or are refactoring away) the security attributes on your MVC controllers this technique for testing is well worth a look.It provides a way of writing simple, readable tests for the MVC security model.



It is fair to say that these tests are not fast, the basic setup of a single test run takes about 30 seconds (but subsequent test in a batch are far faster) so you are not going to run them all the time in a TDD style. I think you should consider them as integration tests and run them as part of you continuous integration process. I think it is a more robust means of testing security than recorder based Web Tests. All thanks to Artem from producing such a useful Typemock add-in.