Building a MVC2 Template, Part 6, Writing Specs Continued

No Comments

Summary

In this post we’ll finish converting the auto-generated tests to MSpec specification.

Automate Running MSpec

Nehemiah.Specs-Properties
If you wish to automate the running of the MSpec tool, open the properties to the Nehemiah.Specs project. Add the line below to the “Post-build event command line:” field.  Now when Nehemiah.Specs builds successfully, MSpec will run automatically.

C:\_CodeVault\MSpec\mspec.exe $(TargetName)$(TargetExt) –html “$(ProjectDir)Report.html”

Converting Tests to Specs

I am writing these posts a couple of week ahead of the publish date.  So hopefully I can smooth out all the bumps before they get published.  However, that is not always the case.  I have noticed the file TextExtensions.cs should have been named TestExtensions.cs.  So unless someone points it out before this post is published, it will be known now.

I want to thank James Broome at http://jamesbroo.me/ for his great posts on Mvc controllers and BDD. Also the code below. I have renamed the file TextExtensions.cs to ActionResultExtensions.cs and added the full source.

using System.Web.Mvc;

namespace Nehemiah.Specs
{

public static class ActionResultExtensions
{
public static ViewResult is_a_view_and(this ActionResult result)
{
return (result as ViewResult);
}

public static RedirectResult is_a_redirect_and(this ActionResult result)
{
return (result as RedirectResult);
}

public static RedirectToRouteResult is_a_redirect_to_route_and(this ActionResult result)
{
return (result as RedirectToRouteResult);
}

public static string controller_name(this RedirectToRouteResult redirect_result)
{
return redirect_result.RouteValues["Controller"].ToString();
}

public static string action_name(this RedirectToRouteResult redirect_result)
{
return redirect_result.RouteValues["Action"].ToString();
}

public static void should_be_empty(this string the_string)
{
the_string.Equals(string.Empty);
}

}   // End Class

}       // End Namespace

Our newly converted specs look like this.


using System.Security.Principal;
using System.Web;
using System.Web.Mvc;
using System.Web.Routing;
using System.Web.Security;
using Machine.Specifications;

using Nehemiah.Controllers;
using Nehemiah.Models;

namespace Nehemiah.Specs.Controllers
{

#region - Change Password Specs -

[Subject("Change Password")]
public class when_the_change_password_page_is_requested : AccountControllerContext
{
static ActionResult result;
static string key;
static int passwordLength;

Establish context = () =>
{
key = "PasswordLength";
passwordLength = 10;
};

Because of = () =>
{
result = controller.ChangePassword();
};

It should_display_the_change_password_page = () =>
{
result.is_a_view_and().ViewName.ShouldBeEmpty();
};

It should_include_a_value_for_a_minimum_password_length = () =>
{
result.is_a_view_and().ViewData[key].ShouldEqual(passwordLength);
};

}

[Subject("Change Password")]
public class change_password_displays_change_password_success_page_on_success : AccountControllerContext
{
static ActionResult result;
static ChangePasswordModel model;

Establish context = () =>
{
model = new ChangePasswordModel()
{
OldPassword = "goodOldPassword",
NewPassword = "goodNewPassword",
ConfirmPassword = "goodNewPassword"
};
};

Because of = () =>
{
result = controller.ChangePassword(model);
};

It should_return_a_RedirectToRouteResult = () =>
{
result.is_a_redirect_to_route_and().action_name().ShouldEqual("ChangePasswordSuccess");
};

}

[Subject("Change Password")]
public class change_password_displays_the_change_password_page_on_failure : AccountControllerContext
{
static ActionResult result;
static string key;
static int passwordLength;
static string failureMessage;
static ChangePasswordModel model;

Establish context = () =>
{
key = "PasswordLength";
passwordLength = 10;
failureMessage = "The current password is incorrect or the new password is invalid.";

model = new ChangePasswordModel()
{
OldPassword = "badOldPassword",
NewPassword = "goodNewPassword",
ConfirmPassword = "goodNewPassword"
};
};

Because of = () =>
{
result = controller.ChangePassword(model);
};

It should_display_the_change_password_page = () =>
{
result.is_a_view_and().ViewName.ShouldBeEmpty();
};

It should_include_a_change_password_failure_message = () =>
{
controller.ModelState[""].Errors[0].ErrorMessage.ShouldEqual(failureMessage);
controller.ModelState.IsValid.ShouldBeFalse();
};

It should_include_a_value_for_a_minimum_password_length = () =>
{
result.is_a_view_and().ViewData[key].ShouldEqual(passwordLength);
};

}

[Subject("Change Password")]
public class change_password_displays_the_change_password_page_on_data_validation_errors : AccountControllerContext
{
static ActionResult result;
static string key;
static int passwordLength;
static ChangePasswordModel model;

Establish context = () =>
{
key = "PasswordLength";
passwordLength = 10;

model = new ChangePasswordModel()
{
OldPassword = "badOldPassword",
NewPassword = "goodNewPassword",
ConfirmPassword = "goodNewPassword"
};
};

Because of = () =>
{
result = controller.ChangePassword(model);
controller.ModelState.AddModelError("", "Dummy error message.");
};

It should_display_the_change_password_page = () =>
{
result.is_a_view_and().ViewName.ShouldBeEmpty();
};

It should_include_previous_form_data_with_validation_errors = () =>
{
result.is_a_view_and().ViewData.Model.Equals(model);
};

It should_include_a_value_for_a_minimum_password_length = () =>
{
result.is_a_view_and().ViewData[key].ShouldEqual(passwordLength);
};

}

[Subject("Change Password")]
public class change_password_displays_the_change_password_success_page_on_success : AccountControllerContext
{
static ActionResult result;

Because of = () =>
{
result = controller.ChangePassword();
};

It should_display_the_change_password_success_page = () =>
{
result.is_a_view_and().ViewName.ShouldBeEmpty();
};

}
#endregion

#region - Logon/Logoff Specs-

[Subject("Logoff")]
public class log_off_will_log_out_the_user_and_return_the_home_page : AccountControllerContext
{

static ActionResult result;

Because of = () =>
{
result = controller.LogOff();
};

It should_log_out_the_user = () =>
{
MockFormsAuthenticationService svc = controller.FormsService as MockFormsAuthenticationService;
svc.SignOut_WasCalled.ShouldBeTrue();
};

It should_return_the_home_page = () =>
{
result.is_a_redirect_to_route_and().controller_name().ShouldEqual("Home");
result.is_a_redirect_to_route_and().action_name().ShouldEqual("Index");
};

}

[Subject("Logon")]
public class when_the_logon_page_is_requested : AccountControllerContext
{

static ActionResult result;
Because of = () =>
{
result = controller.LogOn();
};

It should_return_the_logon_page = () =>
{
result.is_a_view_and().ViewName.ShouldBeEmpty();
};

}

[Subject("Logon")]
public class log_on_returns_redirect_on_success_without_a_return_url : AccountControllerContext
{

static ActionResult result;
static LogOnModel model;

Establish context = () =>
{
model = new LogOnModel()
{
UserName = "someUser",
Password = "goodPassword",
RememberMe = false
};
};

Because of = () =>
{
result = controller.LogOn(model, null);
};

It should_log_in_the_user = () =>
{
MockFormsAuthenticationService svc = controller.FormsService as MockFormsAuthenticationService;
svc.SignIn_WasCalled.ShouldBeTrue();
};

It should_return_the_home_page = () =>
{
result.is_a_redirect_to_route_and().controller_name().ShouldEqual("Home");
result.is_a_redirect_to_route_and().action_name().ShouldEqual("Index");
};

}

[Subject("Logon")]
public class log_on_returns_redirect_on_success_with_a_return_url : AccountControllerContext
{

static ActionResult result;
static LogOnModel model;

Establish context = () =>
{
model = new LogOnModel()
{
UserName = "someUser",
Password = "goodPassword",
RememberMe = false
};
};

Because of = () =>
{
result = controller.LogOn(model, "/MyPage");
};

It should_log_in_the_user = () =>
{
MockFormsAuthenticationService svc = controller.FormsService as MockFormsAuthenticationService;
svc.SignIn_WasCalled.ShouldBeTrue();
};

It should_return_the_requested_url = () =>
{
result.is_a_redirect_and().Url.ShouldEqual("/MyPage");
};

}

[Subject("Logon")]
public class log_on_returns_log_on_page_on_data_validation_errors : AccountControllerContext
{

static ActionResult result;
static LogOnModel model;
static string failureMessage;

Establish context = () =>
{
failureMessage = "The user name or password provided is incorrect.";

model = new LogOnModel()
{
UserName = "someUser",
Password = "",
RememberMe = false
};
};

Because of = () =>
{
result = controller.LogOn(model, null);
};

It should_return_the_logon_page = () =>
{
result.is_a_view_and().ViewName.ShouldBeEmpty();
};

It should_include_previous_form_data_with_validation_errors = () =>
{
controller.ModelState[""].Errors[0].ErrorMessage.ShouldEqual(failureMessage);
controller.ModelState.IsValid.ShouldBeFalse();
};

}

[Subject("Logon")]
public class log_on_returns_log_on_page_on_log_in_failure : AccountControllerContext
{

static ActionResult result;
static LogOnModel model;
static string failureMessage;

Establish context = () =>
{
failureMessage = "The user name or password provided is incorrect.";

model = new LogOnModel()
{
UserName = "someUser",
Password = "badPassword",
RememberMe = false
};
};

Because of = () =>
{
result = controller.LogOn(model, null);
};

It should_return_the_logon_page = () =>
{
result.is_a_view_and().ViewName.ShouldBeEmpty();
};

It should_include_previous_form_data = () =>
{
LogOnModel model = result.is_a_view_and().ViewData.Model as LogOnModel;
model.UserName.ShouldEqual("someUser");
};

It should_include_a_validation_error_message = () =>
{
controller.ModelState[""].Errors[0].ErrorMessage.ShouldEqual(failureMessage);
controller.ModelState.IsValid.ShouldBeFalse();
};

}

#endregion

#region - Registration Specs -

[Subject("User Registration")]
public class when_the_registration_page_is_requested : AccountControllerContext
{
static ActionResult result;

Because of = () =>
{
result = controller.Register();
};

It should_return_the_registration_page = () =>
{
result.is_a_view_and().ViewName.ShouldBeEmpty();
};

}

[Subject("User Registration")]
public class registration_page_returns_user_to_home_page_on_success : AccountControllerContext
{
static ActionResult result;
static RegisterModel model;

Establish context = () =>
{
model = new RegisterModel()
{
UserName = "someUser",
Email = "goodEmail",
Password = "goodPassword",
ConfirmPassword = "goodPassword"
};
};

Because of = () =>
{
result = controller.Register(model);
};

It should_return_the_home_page = () =>
{
result.is_a_redirect_to_route_and().controller_name().ShouldEqual("Home");
result.is_a_redirect_to_route_and().action_name().ShouldEqual("Index");
};

}

[Subject("User Registration")]
public class registration_page_returns_registration_page_if_registration_fails : AccountControllerContext
{
static ActionResult result;
static RegisterModel model;
static string key;
static int passwordLength;
static string failureMessage;

Establish context = () =>
{
model = new RegisterModel()
{
UserName = "duplicateUser",
Email = "goodEmail",
Password = "goodPassword",
ConfirmPassword = "goodPassword"
};

key = "PasswordLength";
passwordLength = 10;
failureMessage = "The current password is incorrect or the new password is invalid.";
};

Because of = () =>
{
controller.ModelState.AddModelError("", "The current password is incorrect or the new password is invalid.");
result = controller.Register(model);
};

It should_return_the_registration_page = () =>
{
result.is_a_view_and().ViewName.ShouldBeEmpty();
};

It should_include_previous_form_data = () =>
{
RegisterModel model = result.is_a_view_and().ViewData.Model as RegisterModel;
model.UserName.ShouldEqual("duplicateUser");
model.Email.ShouldEqual("goodEmail");
};

It should_include_a_validation_error_message = () =>
{
controller.ModelState[""].Errors[0].ErrorMessage.ShouldEqual(failureMessage);
controller.ModelState.IsValid.ShouldBeFalse();
};

It should_include_a_value_for_a_minimum_password_length = () =>
{
result.is_a_view_and().ViewData[key].ShouldEqual(passwordLength);
};
}

[Subject("User Registration")]
public class registration_page_returns_registration_page_on_validation_errors : AccountControllerContext
{
static ActionResult result;
static RegisterModel model;
static string key;
static int passwordLength;
static string failureMessage;

Establish context = () =>
{
model = new RegisterModel()
{
UserName = "someUser",
Email = "goodEmail",
Password = "goodPassword",
ConfirmPassword = "badPassword"
};

key = "PasswordLength";
passwordLength = 10;
failureMessage = "The password and confirmation password do not match.";
};

Because of = () =>
{
controller.ModelState.AddModelError("", "The password and confirmation password do not match.");
result = controller.Register(model);
};

It should_return_the_registration_page = () =>
{
result.is_a_view_and().ViewName.ShouldBeEmpty();
};

It should_include_previous_form_data_with_validation_errors = () =>
{
RegisterModel model = result.is_a_view_and().ViewData.Model as RegisterModel;
model.UserName.ShouldEqual("someUser");
model.Email.ShouldEqual("goodEmail");

controller.ModelState[""].Errors[0].ErrorMessage.ShouldEqual(failureMessage);
controller.ModelState.IsValid.ShouldBeFalse();
};

It should_include_a_value_for_a_minimum_password_length = () =>
{
result.is_a_view_and().ViewData[key].ShouldEqual(passwordLength);
};
}

#endregion

public abstract class AccountControllerContext
{

protected static AccountController controller;

Establish context = () =>
{
controller = new AccountController()
{
FormsService = new MockFormsAuthenticationService(),
MembershipService = new MockMembershipService()
};
controller.ControllerContext = new ControllerContext()
{
Controller = controller,
RequestContext = new RequestContext(new MockHttpContext(), new RouteData())
};

};
}

public class MockFormsAuthenticationService : IFormsAuthenticationService
{
public bool SignIn_WasCalled;
public bool SignOut_WasCalled;

public void SignIn(string userName, bool createPersistentCookie)
{
// verify that the arguments are what we expected
userName.ShouldEqual("someUser");
createPersistentCookie.ShouldBeFalse();

SignIn_WasCalled = true;
}

public void SignOut()
{
SignOut_WasCalled = true;
}
}

public class MockHttpContext : HttpContextBase
{
private readonly IPrincipal _user = new GenericPrincipal(new GenericIdentity("someUser"), null /* roles */);

public override IPrincipal User
{
get
{
return _user;
}
set
{
base.User = value;
}
}
}

public class MockMembershipService : IMembershipService
{
public int MinPasswordLength
{
get { return 10; }
}

public bool ValidateUser(string userName, string password)
{
return (userName == "someUser" && password == "goodPassword");
}

public MembershipCreateStatus CreateUser(string userName, string password, string email)
{
if (userName == "duplicateUser")
{
return MembershipCreateStatus.DuplicateUserName;
}

// verify that values are what we expected
password.ShouldEqual("goodPassword");
email.ShouldEqual("goodEmail");

return MembershipCreateStatus.Success;
}

public bool ChangePassword(string userName, string oldPassword, string newPassword)
{
return (userName == "someUser" && oldPassword == "goodOldPassword" && newPassword == "goodNewPassword");
}
}

}       // End Namespace

Commit all changes to Subversion. Then open http://localhost:8080 and verify that TeamCity build our project without any errors.

References

Shout it


Kick It on DotNetKicks.com
Digg This
Reddit This
Stumble Now!
Buzz This
Vote on DZone
Share on Facebook
Bookmark this on Delicious
Kick It on DotNetKicks.com
Shout it
Share on LinkedIn
Bookmark this on Technorati
Post on Twitter
Google Buzz (aka. Google Reader)

Leave a Reply

Comment moderation is enabled. Your comment may take some time to appear.