Search code examples
asp.net-mvc-3unit-testingasp.net-mvc-4theory

Mvc 4 unit testing concept


I am trying to understand the concept of TDD in MVC 4 application. All examples I've found over the net doesn't explained me the concept of what Unit test in a case of MVC 4 application should test.

So when you writing your unit testing , what are the main points you are trying to test in your controller?

If possible please explain on this example.

public class UsersIDentController : AuthorizedController
{    
    private readonly IUserDetailsService userDetailsService;

    public UsersIDentController(IUserDetailsService userDetailsService,
            IServiceLocator serviceLocator): base(serviceLocator)
    {
    }    

    //
    // GET: /UsersIdentification/
    [AllowAnonymous]
    public ActionResult ShowDetails()
    {
        UsersIdentificationViewModel model = new UsersIdentificationViewModel();
        return View(model);
    }
}

If you'd be writing unit test for this controller (gettin user data) what will you test in unit test.

Thanks.


Solution

  • I can't elaborate much about unit testing on the current example you have. Because all you got there is a single method ShowDetails that returns a View tied with UsersIdentificationViewModel. Since you are returning only View you could change the return type into ViewResult. Then, all you can test is if the model tied with the View is of type UsersIdentificationViewModel.

    I could explain unit testing in MVC little more better if we take a simple example. If you create a fresh MVC application selecting the internet template you would see an AccountController is defined as default and it contains actions for login, register, change password etc.

    Let's take the LogOn action in the AccountController.

    AccountController.cs

    public class AccountController : Controller
    {
        private readonly IAuthProvider _authProvider;
    
        public AccountController(IAuthProvider authProvider)
        {
            _authProvider = authProvider;
        }
    
        [HttpPost]
        public ActionResult LogOn(LogOnModel model, string returnUrl)
        {
            if (ModelState.IsValid)
            {
                if (_authProvider.ValidateUser(model.UserName, model.Password))
                {
                    _authProvider.SetCookie(model.UserName, model.RememberMe);
    
                    if (!String.IsNullOrEmpty(returnUrl))
                    {
                        return Redirect(returnUrl);
                    }
                    else
                    {
                        return RedirectToAction("Index", "Home");
                    }
                }
                else
                {
                    ModelState.AddModelError("", "The user name or password provided is incorrect.");
                }
            }
    
            return View(model);
        }
    
        ...
    }
    

    To avoid the dependency with FormsAuthentication sealed class with the AccountController I've used an interface IAuthProvider to simplify the unit testing.

    IAuthProvider.cs

    public interface IAuthProvider
    {
        bool ValidateUser(string username, string password);
    
        void SetCookie(string username, bool rememberMe);
    
        bool CreateUser(string username, string password, string email, out string error);
    
        bool ChangePassword(string username, string oldPassword, string newPassword);
    
        void SignOut();
    }  
    

    LogOnModel.cs

    public class LogOnModel
    {
        [Required]
        [Display(Name = "User name")]
        public string UserName { get; set; }
    
        [Required]
        [DataType(DataType.Password)]
        [Display(Name = "Password")]
        public string Password { get; set; }
    
        [Display(Name = "Remember me?")]
        public bool RememberMe { get; set; }
    }
    

    You can notice many if..else conditions in the LogOn action and they are good candiates for unit testing.

    I would write atleast four unit tests for the action.

    1. Verify on entering valid credentials the action should redirect to the passed url.
    2. Verify on passing valid credentials without returnUrl the AccountController should redirect the user to the Home action.
    3. Verify on passing invalid credentials the account controller should return view with errors.
    4. Verify when there is a validation error the controller should return view with errors.

    Here are the unit tests I've written using MSTest and RhinoMocks.

    AccountControllerTests.cs

    [TestClass]
    public class AccountControllerTests
    {
       private AccountController _accountController;
       private IAuthProvider _mockAuthProvider;
    
       [TestInitialize]
       public void SetUp()
       { 
           //** Arrange
           _mockAuthProvider = MockRepository.GenerateStub<IAuthProvider>();
           _accountController = new AccountController(_mockAuthProvider);
       }
    
       [TestCleanup]
       public void CleanUp()
       {      
       }
    
       /// <summary>
       /// This test is to verify on entering valid credentials the action should redirect   to the passed url.
       /// </summary>
       [TestMethod]
       public void LogOn_Action_Valid_Credentials_With_ReturnUrl_Test()
       {
           //** Arrange
           var logonModel = new LogOnModel
           {
              UserName = "trigent",
              Password = "password",
              RememberMe = true
           };
    
           // stub the ValidateUser to return "true" to pretend the user is valid.
           _mockAuthProvider.Stub(x => x.ValidateUser(logonModel.UserName, logonModel.Password)).Return(true);
    
           //** Act
           var actual = _accountController.LogOn(logonModel, "/");
    
           //** Assert
    
           // verify RedirectResult is returned from action
           Assert.IsInstanceOfType(actual, typeof(RedirectResult));
    
           // verify the redirect url is same as the passed one.
           Assert.AreEqual(((RedirectResult)actual).Url, "/");
       }
    
       /// <summary>
       /// This test is to verify on passing valid credentials without returnUrl the account controller
       /// should redirect the user to the "Home" action.
       /// </summary>
       [TestMethod]
       public void LogOn_Action_Valid_Credentials_Without_ReturnUrl_Test()
       {
           //** Arrange
           var logonModel = new LogOnModel
           {
               UserName = "trigent",
               Password = "password",
               RememberMe = true
           };
    
           // stub the ValidateUser to return "true" to pretend the user is valid.
           _mockAuthProvider.Stub(x => x.ValidateUser(logonModel.UserName, logonModel.Password)).Return(true);
    
           //** Act
           var actual = _accountController.LogOn(logonModel, string.Empty);
    
           //** Assert
    
           // verify RedirectToRouteResult is returned from action
           Assert.IsInstanceOfType(actual, typeof(RedirectToRouteResult));
    
           // verify the controller redirecting to "Home" action.
           var routeValues = ((RedirectToRouteResult)actual).RouteValues;
           Assert.AreEqual("Home", routeValues["controller"].ToString());
           Assert.AreEqual("Index", routeValues["action"].ToString());
       }
    
       /// <summary>
       /// This test is to verify on passing invalid credentials the account controller should return the login view 
       /// with error messages.
       /// </summary>
       [TestMethod]
       public void LogOn_Action_Invalid_Credentials_Test()
       {
           //** Arrange
           var logonModel = new LogOnModel
           {
              UserName = "trigent",
              Password = "password",
              RememberMe = true
           };
    
           // stub the ValidateUser to return "false" to pretend the user is invalid.
           _mockAuthProvider.Stub(x => x.ValidateUser(logonModel.UserName, logonModel.Password)).Return(false);
    
           //** Act
           var actual = _accountController.LogOn(logonModel, string.Empty);
    
           //** Assert
    
           // verify ViewResult is returned from action
           Assert.IsInstanceOfType(actual, typeof(ViewResult));
    
           // verify the controller throws error.
           var modelStateErrors = _accountController.ModelState[""].Errors;
           Assert.IsTrue(modelStateErrors.Count > 0);
           Assert.AreEqual("The user name or password provided is incorrect.", modelStateErrors[0].ErrorMessage);
       }
    
       /// <summary>
       /// This test is to verify when there is a validation error the controller should return the same login view.
       /// </summary>
       [TestMethod]
       public void LogOn_Action_Invalid_Input_Test()
       {
           //** Arrange
           _accountController.ModelState.AddModelError("UserName", "UserName is Required.");
    
           //** Act
           var actual = _accountController.LogOn(new LogOnModel(), string.Empty);
    
           //** Assert
    
           // verify ViewResult is returned from action
           Assert.IsInstanceOfType(actual, typeof(ViewResult));
       }
    }