Background
ASP.NET MVC is built with the intention of enabling developers to test drive a web application using TDD. We can unit test the model as well as controller code. In most cases unit testing the controller action is quite straightforward. Although ASP.NET MVC discourages use of server controls and state management technique like ViewState there are scenarios where we might need to use some sort of state management across different pages or views. I encountered one such scenario where my team was using custom login instead of built in membership provider to validate users. I had to display currently logged in user information on every view page.
Steps for unit testing SessionState in ASP.NET MVC
Lets try to first see where the problem lies. Assume I have a controller for Login with an action called LogOn. This LogOn method is responsible for validating the user and redirecting the user to appropriate view if successful. We should be able to display the user name on all the subsequent pages. Lets build this sample using TDD.
I am going to use the Repository Pattern to validate users against a user repository. In this example I am not really worried about how the repository is populated with the user objects. In a real scenario it could be a call to data access layer or a service which will provide a list of users to the repository. Many of the examples on the net show repository being implemented with LINQ to SQL code. I will set the repository with initial set of user entity which is called UserEntity. UserEntity is the domain object for storing different user properties.
I have created a separate project in my solution to store all the repository and domain entity objects called Repositories. You can have these classes directly under App_Data folder as well. I need only UserName and Password properties in this example. So for the time being I'll build my domain UserEntity object with only those two properties. The UserEntity class looks like
public class UserEntity
{
public string Name { get; set; }
public string Password { get; set; }
}
Following the Agile methodology of writing only the code which will be required for my method, let me create a UserRepository class with a ValidateUser method. This method will take user name and password as input parameters and return true or false depending on whether the user exists in repository or not. I want to inject this repository in the controller using Dependency Injection principle. In order to follow the best practices of programming to an interface as compared to that of a concrete class, lets abstract this functionality into an interface called IUserRepository.
public interface IUserRepository
{
bool ValidateUser(string userName, string password);
}
Lets create a test to verify that the ValidateUser method behaves as expected. I would want to test for both the positive as well as negative scenarios. Hence I have used the initialization method of NUnit framework called SetUp to initialize the instance of UserRepository.
private IUserRepository _userRepository;
[SetUp]
public void Setup()
{
_userRepository = new UserRepository();
}
If we try to build at this stage we'll get a build error. The reason for that is we don't have the UserRepository class implemented as yet. Lets go ahead and do that. I would like to initialize the repository with one user.
public class UserRepository:IUserRepository
{
private IList<UserEntity> _users;
public UserRepository()
{
_users = new List<UserEntity>()
{
new UserEntity {Name = "Nilesh", Password = "abc@123"}
};
}
}
Since I am implementing the IUserRepository interface I need to implement the ValidateUser method. Add the following method definition to above class and build the solution.
public bool ValidateUser(string userName, string password)
{
var validUser = _users.SingleOrDefault(user => user.Name == userName && user.Password == password);
return !(validUser == null);
}
Now that we have the method as well implemented, lets test both positive as well as negative scenarios.
[Test]
public void ValidateUserShouldReturnTrueForValidUser()
{
//Arrange
string userName = "Nilesh";
string password = "abc@123";
//Act
bool result = _userRepository.ValidateUser(userName, password);
//Assert
Assert.That(result, Is.EqualTo(true), "Result is false");
}
[Test]
public void ValidateUserShouldReturnFalseForInvalidUser()
{
//Arrange
string userName = "Nilesh";
string password = "abcd";
//Act
bool result = _userRepository.ValidateUser(userName, password);
//Assert
Assert.That(result, Is.EqualTo(false), "Result is not null");
}
I could have tested the negative scenario by having an additional test where password matches but the user name is invalid. I'll leave that as an exercise for the readers to implement that method.
Lets get our attention back towards to LoginController. Lets start by writing a test which will test for the constructor to accept IUserRepository as a dependency.
[TestFixture]
public class LoginControllerUnitTests
{
private Mock<IUserRepository> _mockUserRepository;
private LoginController _loginController;
[SetUp]
public void SetUp()
{
_mockUserRepository = new Mock<IUserRepository>();
_loginController = new LoginController(_mockUserRepository.Object);
}
}
If I build the application now, compiler complains about the constructor which doesn't have IUserRepository as parameter. Lets add an overloaded constructor which taken IUserRepository as parameter and make default constructor call pass this instance as shown below
private readonly IUserRepository _userRepository;
public LoginController() : this (new UserRepository())
{}
public LoginController (IUserRepository userRepository)
{
_userRepository = userRepository;
}
Now if we build the solution it will work fine. In the above test code I have used Moq framework to dynamically mock the IUserRepository interface. Now that we have the IUserRepository injected into the LoginController lets write a test which will validate the user against the repository by calling the ValidateUser method. I'll need to collect the user details from the forms collection which are passed from the view to the LogOn action method. If the user is valid we'll redirect the user to home controller's Index view. If the user credentials are invalid, we'll show the error message by updating the ModelState of controller.
[Test]
public void ValidUserDetailsAreStoredinSession()
{
//Arrange
string userName = "Nilesh";
string password = "abc@123";
FormCollection formCollection = new FormCollection();
formCollection["userName"] = userName;
formCollection["password"] = password;
_mockUserRepository.Setup(userRepository => userRepository.ValidateUser(userName, password))
.Returns(true);
//Act
RedirectToRouteResult result =_loginController.LogOn(formCollection) as RedirectToRouteResult;
//Assert
Assert.That(result.RouteValues["controller"], Is.EqualTo("Home"));
Assert.That(result.RouteValues["action"], Is.EqualTo("Index"));
}
If there is no definition of LogOn method the compilation will fail. Let me add the LogOn action as follows
public ActionResult LogOn(FormCollection formCollection)
{
return View();
}
In the test code I have populated the form collection with required values and have set an expectation on the ValidateUser to return true value which means that the user is valid. After that assert that the user is redirected to the Index view of the Home controller. Lets run this test and see the result. I get the following error
Moq.MockVerificationException: The following setups were not matched:
IUserRepository userRepository => userRepository.ValidateUser("Nilesh", "abc@123") (ValidUserDetailsAreStoredinSession() in LoginControllerUnitTests.cs
This can be rectified by calling the respective method on the repository. But for that we'll need to read the values from the form collection. Lets implement the same in our LogOn action. Also if the user is valid we'll redirect the user to Home controller and Index method.
public ActionResult LogOn(FormCollection formCollection)
{
string userName = formCollection["UserName"];
string password = formCollection["Password"];
bool validUser = _userRepository.ValidateUser(userName, password);
if(validUser)
{
return RedirectToAction("Index", "Home");
}
else
{
return View();
}
}
These changes to the code makes the test pass. At this point I want to store the user name who has logged in to a Session variable so that i can use it in all the pages. Lets add the value to Session and see what happens when we rerun the test. I'll add the following line just before calling the RedirectToAction method within the if block.
Session["UserName"] = userName;
Rerun the test and check the impact of this change. Immediately I get an null reference exception "System.NullReferenceException: Object reference not set to an instance of an object."
The reason for this is that we are trying to use an intrinsic object (Session) in out LogOn action. If we make use of intrinsic objects like HTTPSession, HTTPRequest, HTTPResponse etc within our code it becomes tricky to test that code. one method of overcoming this is to abstract these functionality into a wrapper class. This wrapper class can be injected into the controller and can be unit tested using a mock object. I am going to do the same with Session object. Lets wrap it into another class called SessionHelper using two methods Add and Get
public class SessionHelper
{
public void Add(string sessionKey, object sessionValue)
{
HttpContext.Current.Session.Add(sessionKey, sessionValue);
}
public object Get(string sessionKey)
{
return HttpContext.Current.Session[sessionKey];
}
}
Once we have this class we can inject it into the LoginController and make the required changes to the LogOn action as well as unit test method. Instead of showing the complete test and action methods I'll show only the changes. For the complete code you can download the code.
_mockSessionHelper.Setup(sessionHelper => sessionHelper.Add("UserName", userName));
_sessionHelper.Add("UserName",userName);
The first line needs to be added to test method and the second one to the LogOn action. We'll also need to instantiate these two variables in the respective classes. Also the dependency injection of Session helper needs to be taken care. i have added all these steps to the sample code which is attached at the end of this post.
ASP.NET MVC supports sharing data between successive request by means of a data structure called TempData. But the limitation of this approach is that it can share data only between two successive requests.
Conclusion
My intention here is to show how we can unit test controller action involving SessionState. There are alternatives to this approach. We can make use of TestHelpers from MVCContrib which can mock any of the framework classes like HTTPContext, Session, Request, Response etc. I haven't gone into the details of creating the views for this sample as I wanted to concentrate on unit testing the controller bit. In some future post I might demonstrate the views for this sample.
You can download the code from my dropbox.
If you are having problems accessing the link you could also try pasting the url http://dl.getdropbox.com/u/1569964/SessionStateUnitTest.zip in your browser.