Unit testing MonoRail controllers – Redirects


When developing with MonoRail, one of the common operations is to redirect to other controllers and actions.  Originally, I looked at the BaseControllerTester to help test, but it required a little too much knowledge of the inner workings of MonoRail for my taste.  Instead, I’ll use a common Legacy Code technique to achieve the same effect.

The first attempt

The easiest way to see if a method is testable is just to try it out.  Right now I don’t have much of an idea of what to test.  I do know that the method I want to call in the Controller base class is “Redirect”.  I don’t really know what that does underneath the covers, but I don’t much care.  What I’d like to do is create a PartialMock for the AccountController, and just make sure that the “Redirect” method is called with the correct parameters.

A side note, PartialMock is great for mocking classes (as opposed to interfaces).  I can selectively remove behavior for specific methods while leaving the other methods and behavior in place.

Here’s my first attempt at a test with AccountController getting mocked out:

[TestFixture]
public class When_authenticating_with_valid_credentials
{
private MockRepository _mocks;
private IUserRepository _userRepo;
private AccountController _acctCtlr;

[SetUp]
public void Before_each_spec()
{
_mocks = new MockRepository();

_userRepo = _mocks.CreateMock<IUserRepository>();
_acctCtlr = _mocks.PartialMock<AccountController>(_userRepo);

User user = new User();
user.Username = "tester";
user.Password = "password";

Expect.Call(_userRepo.GetUserByUsername("tester")).Return(user);
Expect.Call(() => _acctCtlr.Redirect("main", "index")).Repeat.AtLeastOnce();

_mocks.ReplayAll();
}

[Test]
public void Should_redirect_to_landing_page()
{
_acctCtlr.Login("tester", "password");

_mocks.VerifyAll();
}
}

In the Before_each_spec method, I set up the appropriate mocks and use the PartialMock method to try and mock out the AccountController.  Additionally, I set up expectations for the IUserRepository and additionally for the AccountController.

Unfortunately, the test fails, but not for the reasons I like.  I get all sorts of exceptions from deep inside the MonoRail caves.  The mocking should have worked, so what went wrong?

A little workaround

Digging deeper into the MonoRail API, I find the culprit.  Although I told Rhino Mocks to intercept the Redirect call, it will only work for virtual and abstract methods.  The Redirect method is neither.

Pulling an old trick out of the hat, I wrote quite a while ago about subclassing and overriding non-virtual methods.  This trick will work just great here.  I’ll create a seam between the AccountController and MonoRail’s SmartDispatcherController:

public class BaseController : SmartDispatcherController
{
public virtual new void Redirect(string controller, string action)
{
base.Redirect(controller, action);
}
}

Additionally, I change the AccountController to inherit from this new seam class.  Since I just wrap and delegate to the base class, production code won’t be affected in the slightest.  Since my test mocks AccountController, which has a virtual “Redirect” method, the test now fails correctly.  And now that it fails correctly, I can fill out the implementation:

public class AccountController : BaseController
{
private readonly IUserRepository _repo;

public AccountController(IUserRepository repo)
{
_repo = repo;
}

public void Login(string username, string password)
{
User user = _repo.GetUserByUsername(username);

if (user != null)
{
// check password
Redirect("main", "index");
}
}
}

Although the BaseControllerTester provides a lot of out-of-the-box testing functionality, sometimes the implementation details can leak into my tests, which is what I don’t want to happen.  I like to keep my tests coupled to frameworks as little as possible, and there’s nothing really specific to MonoRail in my tests.  The method might change, but the MVC frameworks I’ve looked at all have some kind of “redirect to some other controller and action” method.

The subclass-and-override technique provides a quick way to introduce a seam into classes that don’t offer out-of-the-box testability in the places you want.  Since this pattern doesn’t affect production code, I can feel confident testing my Controllers (or anything else similar) in this manner.

Eliminating obscure tests