TDD/BDD with ASP.NET MVC

I have recently been going hammer and tongs with ASP.NET MVC. One of the selling points for ASP.NET MVC is its testability, so while I have been learning this technology, I have found a couple of annoyances.

When you create a new ASP.NET MVC project, the wizard in VS2008 asks you if you want to create a test project. If you do create the test project, you get a couple of pre made tests that really don’t portray good TDD and because you only get a couple of tests, you have low code coverage and have untested code. I have had to back peddle  tests for the existing code.

For doing any development with ASP.NET MVC, the MvcContrib project is an absolute must. The MvcContrib project contains general features that really improve your development, Dependency Injection, type safety, a Grid and much more. I found that the MvcContrib examples, are so straight forward that in 60 seconds of eyeballing the code I could understand it fully.

Keeping to the subject, MvcContrib provides a library that really help when it comes to testing. I have been developing with MonoRail fairly solidly for the last 6 months or so and their are similarities between the Castle test classes and the Library from MvcContrib.

To make things more real world:

  • My project is using Unity for its IoC container and I am injecting dependancies into my controllers (again the MvcContrib provides libraries that make this easy to implement). 
  • For writing my tests/specs, I am using NBehave, NUnit and RhinoMocks 3.5. 

I started rewriting the tests for the HomeController because is was the easiest. The tests check that the controller displays the right view.

using System.Web.Mvc;
using RussellEast.Mvc.Controllers;
using MvcContrib.TestHelper;
using NBehave.Narrator.Framework;
using NBehave.Spec.NUnit;
using NUnit.Framework;
using Specification = NUnit.Framework.TestAttribute;

namespace RussellEast.Mvc.Tests.Controllers.Home
{
    [TestFixture]
    public class When_displaying_home_pages : SpecBase
    {
        private HomeController controller;
        private Story story;
        private TestControllerBuilder builder;

        [Story]
        public override void MainSetup()
        {
            base.MainSetup();

            story = new Story("Home page");

            story.AsA("user")
                .IWant("to go to the home page")
                .SoThat("i can start using the application");

            PrepareController();
        }

        private void PrepareController()
        {
            builder = new TestControllerBuilder();

            controller = builder.CreateController();
        }

        [Specification]
        public void Index()
        {
            ActionResult result = null;

            story.WithScenario("home index page")
                .Given("the controller has been prepared", () => controller.ShouldNotBeNull())
                .When("the user navigates to the index view", () => result = controller.Index())
                .Then("verify that the result is correct", () => result.AssertViewRendered().ForView("Index"));
        }

        [Specification]
        public void About()
        {
            ActionResult result = null;

            story.WithScenario("about page")
                .Given("the controller has been prepared", () => controller.ShouldNotBeNull())
                .When("the user navigates to the about view", () => result = controller.About())
                .Then("verify that the result is correct", () => result.AssertViewRendered().ForView("About"));
        }
    }
}

The controller code is:

 

    public class HomeController : Controller
    {
        [Authorize, DefaultAction]
        public ActionResult Index()
        {
            ViewData["Title"] = "Home Page";
            ViewData["Message"] = "Sample text being displayed";

            return View("Index");
        }

        public ActionResult About()
        {
            ViewData["Title"] = "About Page";

            return View("About");
        }
    }

Note: to order for the above tests to pass, you need to supply the name of the view to render.

The tests above cover the behaviours for the HomeController which are very basic, so to raise the bar. The ASP.NET MVC project template comes with an AccountController that works with a membership provider and Forms Authentication. The controller has a constructor that accepts two parameters. Because the “System under test” is the Account Controller i am going to stub the dependencies and write tests for the “Login” and  “Logout” actions.

 

using System.Web.Mvc;
using System.Web.Security;
using RussellEast.Mvc.Controllers;
using MvcContrib.TestHelper;
using NBehave.Narrator.Framework;
using NBehave.Spec.NUnit;
using NUnit.Framework;
using Rhino.Mocks;
using Specification = NUnit.Framework.TestAttribute;

namespace EWS.Steeple.Tests.Controllers.Authentication
{
    [TestFixture]
    public class When_authenticating : SpecBase
    {
        private AccountController controller;
        private IFormsAuthentication formsAuthentication;
        private MembershipProvider membershipProvider;

        private Story story;

	[Story]
        public override void MainSetup()
        {
            base.MainSetup();

            story = new Story("Authenticating");

            story.AsA("known user to the system")
                .IWant("to be authenticated")
                .SoThat("i can use the system");

            PrepareController();
        }

        private void PrepareController()
        {
            TestControllerBuilder builder = new TestControllerBuilder();

            formsAuthentication = MockRepository.GenerateStub();
            membershipProvider = MockRepository.GenerateStub();

            controller = builder.CreateController(new object[] {formsAuthentication, membershipProvider});
        }

        [Specification]
        public void Should_log_in_with_user_name_and_password_while_not_caching_details()
        {
            string username = string.Empty;
            string password = string.Empty;
            bool rememberMe = false;

            ActionResult result = null;

            story.WithScenario("log in with validate details")
                .Given("a controller instance", ()=> controller.ShouldNotBeNull())
                .And("a user name", () => username = "Username")
                .And("a password", () => password = "password")
                .When("i attempt to log in", () =>
                {
                    membershipProvider.Expect(x => x.ValidateUser(username, password)).Return(true);
                    formsAuthentication.Expect(x => x.SetAuthCookie(username, rememberMe));

                    result = controller.Login(username, password, rememberMe, string.Empty);
                })
                .Then("redirect user to the home page", () =>
                {
                    result.AssertActionRedirect().ToAction(x => x.Index());

                    formsAuthentication.VerifyAllExpectations();
                    membershipProvider.VerifyAllExpectations();
                });
        }

        [Specification]
        public void Should_sign_out_and_return_to_login_when_logging_out()
        {
            ActionResult result = null;

            story.WithScenario("log out and return to login view")
                .Given("a controller instance", () => controller.ShouldNotBeNull())
                .When("i log out", () =>
                {
                    formsAuthentication.Expect(x => x.SignOut());

                    result = controller.Logout();
                })
                .Then("i expect to be sent to the login view", () =>
                {
                    result.AssertActionRedirect().ToAction("Login");

                    formsAuthentication.AssertWasCalled(x => x.SignOut());
                });
        }
    }
}

Some points to be aware of.

  • I have used two different ways to test the ActionResult was a redirect. The first and my preferred way is using the generic controller type and the lambda expression. I have been forced to use the string argument approach due to the fact that the “Login” action is overloaded. It complained when trying to use a lambda approach.;-(.
  • MvcContrib test library provides a “TestControllerBuilder” that not only creates that instance of the controller, but mocks out the HttpContext, Sessions and form data. The docs for MvcContrib explains all. I have used this in my “PrepareController” method. 
  • The default code in the AccountController has not been changed to suit these tests.