How do I Unit Test the correct view is returned with MVC ASP.Net?

11,010

I’m new to MVC, Unit Testing, Mocking and TDD. I’m trying to follow best practice as closely as possible.

I feel happy that more and more developers are starting to write unit tests for their code, so congratulations you are on the right path.

if I don’t specify the view name in the controller. If I do specify the ViewName in the controller the test always passes, even if the view doesn’t exist.

When you do not specify a view name in the View method this instructs the MVC engine to render the default view, so for example

public ActionResult Index() { return View(); }

The above code will return an empty view name meaning that the rendered view will be the name of the action, in this case it will be Index.

So if you want to test that an action returns the default view, you have to test that the returned view name is empty

Test always passes as view name is specified even if the view doesn't exist.

In order to explain what's going on here, I'll explain first how the action filters work.

There are basically four types of filters

  • Exception filters
  • Authorization filters
  • Action filters
  • Result filters

I will concentrate on action and result filters

Action filters are defined with the IActionFilter interface

public interface IActionFilter
{
    // Summary:
    //     Called after the action method executes.
    //
    void OnActionExecuted(ActionExecutedContext filterContext);
    //
    // Summary:
    //     Called before an action method executes.
    //
    void OnActionExecuting(ActionExecutingContext filterContext);
}

Result filters are defined with the IResultFilter interface

public interface IResultFilter
{
    // Summary:
    //     Called after an action result executes.
    //
    void OnResultExecuted(ResultExecutedContext filterContext);
    //
    // Summary:
    //     Called before an action result executes.
    //
    void OnResultExecuting(ResultExecutingContext filterContext);
}

When a controller's action is executed the following filters are executed in this specific order:

IActionFilter.OnActionExecuting
IActionFilter.OnActionExecuted
IResultFilter.OnResultExecuting
IResultFilter.OnResultExecuted

When an action is executed, another component is in charge to process your ActionResult returned from your action and render the correct HTML to send it back to the client, this is when the result is processed

This clean separation of concerns is the beauty and the key to allow us unit test our controller's actions, otherwise, if they were coupled, we won't be able to unit test in isolation the result of the actions

Now the RazorViewEngine tries to find a view after an action has been executed (when the result is being processed) that's why your tests return true even when the physical view does not exist. This is the expected behavior, and remember you need to test in isolation your controller's actions. As long as you assert in your unit tests that the expected view is rendered you are done with your unit tests.

If you want to assert that the physical view exists then you would be talking about some specific integration tests: functional tests or user acceptance tests - these kind of tests require the instantiation of your application using a browser they are not in any way unit tests

Now it's OK that you are writing your unit tests manually (this is a great exercise if you are getting into the unit testing world), however, I'd like to recommend you a couple of MVC Testing frameworks that can help you write your unit tests really fast

A few personal comments about these frameworks

According to my experience, MVC Contrib has more features than Fluent MVC Testing, however, since I'm using MVC 4 I have not been able to get it working in Visual Studio 2012, so I use a combination of both (this is a dirty workaround until I find a better approach)

This is what I do:

var testControllerBuilder = new TestControllerBuilder(); // this is from MVC Contrib
var controller = new MoviesController(
    this.GetMock<IMovieQueryManager>().Object);

testControllerBuilder.InitializeController(controller); // this allows me to use the Session, Request and Response objects as mock objects, again this is provided by the MVC Contrib framework

// I should be able to call something like this but this is not working due to some problems with DLL versions (hell DLL's) between MVC Controb, Moq and MVC itself
// testControllerBuilder.CreateController<MoviesController>();

controller.WithCallTo(x => x.Index(string.Empty)).ShouldRenderDefaultView(); // this is using Fluent MVC Testing

// again instead of the above line I could use the MVC Contrib if it were working....
// var res = sut.Index(string.Empty);
// res.AssertViewRendered().ForView("Index");

I hope this helps =) Happy coding !

Share:
11,010
TheLukeMcCarthy
Author by

TheLukeMcCarthy

Luke works as a software and web developer. His goal is to automate repetitive mundane tasks to empower people to become more productive, do more interesting higher value activities, and help people leave work on time. Some of his most interesting technology discoveries can be found on his blog http://my-side-projects.blogspot.com Luke is also a self-proclaimed foodie and chocoholic, he insists he can give up chocolate up anytime he wants however currently has no plans to do so.

Updated on June 28, 2022

Comments

  • TheLukeMcCarthy
    TheLukeMcCarthy almost 2 years

    I’m new to MVC, Unit Testing, Mocking and TDD. I’m trying to follow best practice as closely as possible.

    I’ve written a unit test for a controller and I’m having trouble testing if the correct view is returned. If I use the ViewResult.ViewName the test always fails if I don’t specify the view name in the controller. If I do specify the ViewName in the controller the test always passes, even if the view doesn’t exist.

    I’ve also tried testing the Response.Status code however this always returns 200 (code taken from Darin Dimitrov’s answer to MVC3 unit testing response code). What I’m aiming for is the classic red, green refactor when creating a new view and avoiding 404 and System.InvalidOperationException errors when going live, is this possible?

    Code Below.

    public class BugStatusController : Controller
    {
        public ActionResult Index(){
            return View(); // Test always fails as view name isn’t specified even if the correct view is returned.
        }
    
        public ActionResult Create(){
            return View("Create"); // Test always passes as view name is specified even if the view doesn’t exist.
        }
    }
    
    [TestFixture]
    public class BugStatusTests
    {    
        private ViewResult GetViewResult(Controller controller, string controllerMethodName){
            Type type = controller.GetType();
            ConstructorInfo constructor = type.GetConstructor(Type.EmptyTypes);
    
            object instance = constructor.Invoke(new object[] {});
            MethodInfo[] methods = type.GetMethods();
    
            MethodInfo methodInfo = (from method in methods
                                    where method.Name == controllerMethodName
                                                        && method.GetParameters().Count() == 0
                                    select method).FirstOrDefault();
    
            Assert.IsNotNull(methodInfo, "The controller {0} has no method called {1}", type.Name, controllerMethodName);
    
            ViewResult result = methodInfo.Invoke(instance, new object[] {}) as ViewResult;
    
            Assert.IsNotNull(result, "The ViewResult is null, controller: {0}, view: {1}", type.Name, controllerMethodName);
    
            return result;
        }
    
        [Test]
        [TestCase("Index", "Index")]
        [TestCase("Create", "Create")]
        public void TestExpectedViewIsReturned(string expectedViewName, string controllerMethodName){
            ViewResult result = GetViewResult(new BugStatusController(), controllerMethodName);
    
            Assert.AreEqual(expectedViewName, result.ViewName, "Unexpected view returned, controller: {0}, view: {1}", CONTROLLER_NAME, expectedViewName);
        }
    
        [Test]
        [TestCase("Index", "Index")]
        [TestCase("Create", "Create")]
        public void TestExpectedStatusCodeIsReturned(string expectedViewName, string controllerMethodName)
        {
            var controller = new BugStatusController();
            var request = new HttpRequest("", "http://localhost:58687/", "");
            var response = new HttpResponse(TextWriter.Null);
            var httpContext = new HttpContextWrapper(new HttpContext(request, response));
            controller.ControllerContext = new ControllerContext(httpContext, new RouteData(), controller);
    
            ActionResult result = GetViewResult(controller, controllerMethodName);
    
            Assert.AreEqual(200, response.StatusCode, "Failed to load " + expectedViewName + " Error: "  + response.StatusDescription);
        }
    }
    
  • craastad
    craastad almost 11 years
    Seriously great post! Learned way more than I was searching for!
  • Christopher Francisco
    Christopher Francisco over 9 years
    If I understood correctly, then I should not be testing if the viewName is "index". I should be asserting that viewname is empty?
  • Jupaol
    Jupaol about 9 years
    When you want to assert the default view is returned yes. If it's not a default view, then assert the correct view is returned by using the view name
  • JCisar
    JCisar about 9 years
    To assert that a specific view is returned by view name you would need to do something like this: var result = (ViewResult)result; Assert.AreEqual("viewName", result.ViewName);