Asp.Net MVC Controllers + BDD = The perfect match? Part 2: Log On and Log Off with the AccountController
Tags: .NET, Agile, Asp.Net, BDD, C#, MVC, TDD, Testing September 23rd, 2009This is part 2 in a series of posts on using Behaviour Driven Development to build and test your MVC controllers. The full series is as follows:
- Asp.Net MVC Controllers + BDD = The perfect match? Part 1: The HomeController
- Asp.Net MVC Controllers + BDD = The perfect match? Part 2: Log On and Off with the AccountController
- Asp.Net MVC Controllers + BDD = The perfect match? Part 3: The AccountController contd.
- Asp.Net MVC Controllers + BDD = The perfect match? Part 4: The source code (coming soon…)
In my last post I talked about why I thought that BDD was a great choice when it comes to testing your MVC controllers. The controllers in an MVC application are what handle the user actions and inputs and determine what the correct result should be e.g. displaying a view or redirecting to another action. The controllers are therefore what govern the flow of your application from the end user’s perspective, which makes them ideal candidates for BDD style specifications as the language and business requirements are relevant to and can be validated by a non-technical team member (e.g. the Product Owner).
I began by looking at the out-the-box Asp.Net MVC application that you get when you select “File | New Project | ASP.Net MVC Web Application” in Visual Studio as this provides us with some basic, yet widely understood functionality that I can use to illustrate my point. I started by writing specifications for the out-the-box HomeController to prove that it returned the correct views for the Index and About actions, and that the correct data was passed into the ViewData.
As I mentioned before, this exercise is slightly backwards, as the functionality we are writing specifications for already exists, however, the intention is to show how beneficial BDD can be at this layer of the application and how it could be applied going forward as you build out the rest of your functionality.
Recap
I’m using JP Boohoo’s developwithpassion BDD library to give me a style and syntax for my BDD specs. If you’re new to BDD, I’d recommend checking out his blog first for some background info on what this library is all about.
I’d also created some MVC specific extension methods for testing my HomeController so that I can easily prove the type of ActionResult my actions return and check the type-specific properties on them in a fluent syntax e.g.
it should_return_the_home_view = () =>
result.is_a_view_and().ViewName.should_be_empty();
The AccountController
The AccountController is more interesting than the HomeController as it actually does something! If we take a look at the out-the-box functionality it contains, we can see that it provides the following account operations:
- Log on a User
- Log off a User
- Register a new User
- Change a User’s password
- Prevent Window’s authenticated Users from accessing the application
So, I’ll start in this post by dealing with the Log on and Log off functionality.
Set up
The out-the-box AccountController provides two constructors, which makes it easy to test. It has two dependencies – a FormsAuthenticationService and a MembershipService, which at runtime will be instantiated using the default forms authenticaiton and membership providers. However, during testing, we can set these to be whatever we want. Our AccountController specifications are all going to use mock versions of these dependencies, which will allow us to set up and simulate the contexts that we need to fulfil our specifications. I start by creating a “base” context that can be used by all my AccountController specifications:
public abstract class concern_for_account_controller : observations_for_a_sut_without_a_contract<AccountController>
{
protected static IFormsAuthentication forms_authentication;
protected static IMembershipService membership_service;
protected static string user_name;
protected static string password;
protected static bool remember_me;
protected static string return_url;
protected static string email;
protected static string confirm_password;
protected static string new_password;
protected static string confirm_new_password;
context c = () =>
{
forms_authentication = the_dependency<IFormsAuthentication>();
membership_service = the_dependency<IMembershipService>();
membership_service.Stub(ms => ms.MinPasswordLength).Return(4);
user_name = "name";
password = "password";
confirm_password = "password";
new_password = "newpassword";
confirm_new_password = "newpassword";
email = "email";
remember_me = false;
return_url = "/";
};
}
The developwithpassion BDD library will take care of a lot of set up work for us. I initialise the IFormsAuthentication and IMembershipService using the the_depdendency() method, which tells the BDD library to create a new mock version of each of these interaces using RhinoMocks, and also to use them in the constructor of the system under test (in this case, the AccountController). So, whenever I access the system under test in one of my specifications, it will already be created and have these mock instances as the dependencies that the AccountController needs.
Log On
This first set of specifications I created covers the two LogOn actions. The first is a GET request action which simply displays the Log On view. The second only accepts POST requests and performs the actual log on, providing everything is valid. Here’s the out-the-box functionality that we’re going to be testing:
public ActionResult LogOn()
{
return View();
}
[AcceptVerbs(HttpVerbs.Post)]
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1054:UriParametersShouldNotBeStrings",
Justification = "Needs to take same parameter type as Controller.Redirect()")]
public ActionResult LogOn(string userName, string password, bool rememberMe, string returnUrl)
{
if (!ValidateLogOn(userName, password))
{
return View();
}
FormsAuth.SignIn(userName, rememberMe);
if (!String.IsNullOrEmpty(returnUrl))
{
return Redirect(returnUrl);
}
else
{
return RedirectToAction("Index", "Home");
}
}
Remember – this is the out-the-box Asp.Net MVC functionality that we get with a new project. Working backwards from this functionality, I came up with the following set of specifications…
1. The first action (for a GET request) is pretty simple – we’re just proving that the return is a ViewResult and that the view name is the same as the action name (in this case “LogOn”) by relying on the convention over configuration approach of Asp.Net MVC. If we don’t supply a specific view name for a view result, then the framework will look for one with the same name as the action.
[Concern(typeof(AccountController))]
public class when_the_account_controller_is_told_to_display_the_log_on_view : concern_for_account_controller
{
static ActionResult result;
because b = () =>
result = sut.LogOn();
it should_display_the_log_on_view = () =>
result.is_a_view_and().ViewName.should_be_empty();
}
2. We then need to test the “happy day” scenario. What happens when everything goes as expected? In this case, we validate the user successfully, log the user on successfully and redirect to where they came from. We can override the base context and tell the membership service that we we ask it to validate the user it will return a successful response.
[Concern(typeof(AccountController))]
public class when_the_account_controller_is_told_to_log_on_a_user : concern_for_account_controller
{
static ActionResult result;
context c = () =>
membership_service.Stub(ms => ms.ValidateUser(user_name, password)).Return(true);
because b = () =>
result = sut.LogOn(user_name, password, remember_me, return_url);
it should_validate_the_users_credentials = () =>
membership_service.was_told_to(ms => ms.ValidateUser(user_name, password));
it should_log_the_user_on_to_the_system = () =>
forms_authentication.was_told_to(fa => fa.SignIn(user_name, remember_me));
it should_redirect_the_user_back_to_where_they_came_from = () =>
result.is_a_redirect_and().Url.should_be_equal_to(return_url);
}
3. Next, we can change the context slightly, this time to blank out the return url. This simulates the user logging on without browsing any other part of the site first. In this case, we validate and authenticate the user in the same way, but we redirect to the home page.
[Concern(typeof(AccountController))]
public class when_the_account_controller_is_told_to_log_on_a_user_and_they_havent_come_from_anywhere : concern_for_account_controller
{
static ActionResult result;
context c = () =>
{
return_url = string.Empty;
membership_service.Stub(ms => ms.ValidateUser(user_name, password)).Return(true);
};
because b = () =>
result = sut.LogOn(user_name, password, remember_me, return_url);
it should_validate_the_users_credentials = () =>
membership_service.was_told_to(ms => ms.ValidateUser(user_name, password));
it should_log_the_user_on_to_the_system = () =>
forms_authentication.was_told_to(fa => fa.SignIn(user_name, remember_me));
it should_redirect_the_user_to_the_home_page = () =>
{
result.is_a_redirect_to_route_and().controller_name().should_be_equal_to("Home");
result.is_a_redirect_to_route_and().action_name().should_be_equal_to("Index");
};
}
4. Then we need to start checking our error case scenarios – what happens if we try to log on without specifying a username?
[Concern(typeof(AccountController))]
public class when_the_account_controller_is_told_to_log_on_a_user_with_no_username : concern_for_account_controller
{
static ActionResult result;
context c = () =>
user_name = string.Empty;
because b = () =>
result = sut.LogOn(user_name, password, remember_me, return_url);
it should_display_the_log_on_view = () =>
result.is_a_view_and().ViewName.should_be_empty();
it should_display_the_username_required_validation_message_in_the_view = () =>
result.is_a_view_and().ViewData.ModelState["username"].should_not_be_null();
}
5. And the same for password:
[Concern(typeof(AccountController))]
public class when_the_account_controller_is_told_to_log_on_a_user_with_no_password : concern_for_account_controller
{
static ActionResult result;
context c = () =>
password = string.Empty;
because b = () =>
result = sut.LogOn(user_name, password, remember_me, return_url);
it should_display_the_log_on_view = () =>
result.is_a_view_and().ViewName.should_be_empty();
it should_display_the_password_required_validation_message_in_the_view = () =>
result.is_a_view_and().ViewData.ModelState["password"].should_not_be_null();
}
6. If the user does supply a username and password then we need to try to validate it via the membership service. The credentials they’ve supplied may be invalid, which we can simulate by stubbing out the response from the membership service:
[Concern(typeof(AccountController))]
public class when_the_account_controller_is_told_to_log_on_a_user_who_does_not_exist : concern_for_account_controller
{
static ActionResult result;
context c = () =>
membership_service.Stub(ms => ms.ValidateUser(user_name, password)).Return(false);
because b = () =>
result = sut.LogOn(user_name, password, remember_me, return_url);
it should_try_to_validate_the_users_credentials = () =>
membership_service.was_told_to(ms => ms.ValidateUser(user_name, password));
it should_display_the_log_on_view = () =>
result.is_a_view_and().ViewName.should_be_empty();
it should_display_the_invalid_username_validation_message_in_the_view = () =>
result.is_a_view_and().ViewData.ModelState["_FORM"].should_not_be_null();
}
Log off
7. Finally, for completeness, we can test the expected behaviour for logging off a user, which tells the authentication to sign the user out and then redirects back to the home page:
[Concern(typeof(AccountController))]
public class when_the_account_controller_is_told_to_log_off_a_user : concern_for_account_controller
{
static ActionResult result;
because b = () =>
result = sut.LogOff();
it should_log_the_user_out_of_to_the_system = () =>
forms_authentication.was_told_to(fa => fa.SignOut());
it should_redirect_the_user_to_the_home_page = () =>
{
result.is_a_redirect_to_route_and().controller_name().should_be_equal_to("Home");
result.is_a_redirect_to_route_and().action_name().should_be_equal_to("Index");
};
}
Output
If I run my specifications through the Gallio Icarus test runner I can see that they are being executed successfully:
But more interestingly, I can use JP’s bdddoc report generator to produce an easy to read, BDD specification report based on my specifications in code:
Summary
This is where the real benefit of BDD comes in. My specifications in code are understandable and easily readable when ran through a simple report parser. They can be discussed and validated by developers and non-developers alike. I could show this to the guys behind Asp.Net MVC and see if I’ve understood their desired behaviour of the out-the-box AccountController! If not, we figure out what’s wrong or missing and carry on. At this level of the application the specs really add value in the context of the business requirements of your system. This is why I think that BDD is a great technique for testing the controllers of your MVC application.
In the next post I’ll look at the remaining functionality in the AccountController. I’ll be posting a full working code sample of these specs at the end of the series.

Recent Comments