MonoRail Controller Test Analysis – Problem and Resolution


Last night, my fellow LosTechies geek Jason, wanted

me to check out something he was trying to do in a MonoRail controller

test. 

For some background and the original source code he was working with, see [his Castle forum

post](http://forum.castleproject.org/viewtopic.php?t=3644).

So here was the partial test he was working with:

[Test]
public void Should_have_error_summary_message_when_required_name_is_not_filled()
{
contact.Name = "";
contactController.SendContactMessage(contact);
Assert.AreEqual(@"Contactcontactform", contactController.SelectedViewName);
...
}

 

And here is the target controller code being tested:

public void SendContactMessage([DataBind("contact", Validate = true)] Contact contact)
{
if (HasValidationError(contact))
{
Flash["contact"] = contact;
Flash["summary"] = GetErrorSummary(contact);
RedirectToAction("contactform");
}

// send some email, etc...

RenderView("confirmation");
}

So let’s tackle 3 issues in this scenario, one by one.

Correcting the conditional logic

Since the error validation check is not returning out of the method, as it

stands now, the email will always be sent and the “confirmation” view will

always be rendered no matter what.  I made the exact same kinds of mistakes when

I was first learning MonoRail; expecting that as soon as I call

RedirectToAction, it would take care of performing the redirect right then.  But

of course, essentially, that’s just a method call to notify the framework of

what should be done when the action execution is complete.

So this is an easy one to solve.  We can either throw in a return statement

inside the conditional making it a guard clause, or just wrap the rest of the

logic inside an “else” block.

public void SendContactMessage([DataBind("contact", Validate = true)] Contact contact)
{
if (HasValidationError(contact))
{
Flash["contact"] = contact;
Flash["summary"] = GetErrorSummary(contact);
RedirectToAction("contactform");
return;
}

// send some email, etc...

RenderView("confirmation");
}

OR

public void SendContactMessage([DataBind("contact", Validate = true)] Contact contact)
{
if (HasValidationError(contact))
{
Flash["contact"] = contact;
Flash["summary"] = GetErrorSummary(contact);
RedirectToAction("contactform");
}
else
{
// send some email, etc...

RenderView("confirmation");
}
}

I tend to like returning guard clauses shown in the first example better, but

either way is of course acceptable.

Differences between testing Redirects and Renders

The test method is currently performing an assert on the SelectedViewName of

the controller to make sure the “contactform” view is being displayed.  The

problem here is that the controller is actually doing a

Redirect, not a Render.  There is a difference

in how those are tested. 

  • How to assert that the controller redirected to a

    particular view:

Assert.AreEqual("/Contact/contactform.rails", Response.RedirectedTo);
  • How to assert that a particular view was only

    rendered:

Assert.AreEqual(@"Contactconfirmation", contactController.SelectedViewName);

So this test method we’re working with needs to be changed to use the first

technique of asserting against a redirect, since that’s what’s happening in the

controller.  Easy enough…

Simulating validation errors in the controller 

Ok, so now for the interesting one.  Right now, the call to

HasValidationError inside the controller is always going to

return false.  Because that property relies directly upon a dictionary of error

summaries populated by the databinder.  The reason this an issue in our test

method here, is that if you just call a controller’s action method directly from

a unit test, the databinder is not run, so the actual validation never takes

place.  So even though the contact object that is being passed in really doesn’t

pass validation, the controller doesn’t know that if you relying solely on the

[DataBind] attribute to take care of the validation for you.  Of course you

could run the validation yourself inside of the controller’s action method as an

alternative.  But there is an easier way.  Besides, you have to really ask

yourself, “what should I really be testing here?”. 

Jason already understood this, but for those who may not.  Do you really care

about testing specific validation rules in your controller, like “is the contact

name empty?”?  Well, you probably shouldn’t.  Those should be saved for your

actual domain object validation tests.  All we care about in this controller

test is that if the validation fails for whatever reason (we don’t care why),

then show the error summary and redirect back to the contact form.

Here’s one way that this can be simulated in the controller test:

[Test]
public void Should_load_error_summary_when_contact_is_not_valid()
{
SimulateOneValidationErrorFor(contactController, contact);
contactController.SendContactMessage(contact);

Assert.AreEqual("/Contact/contactform.rails", Response.RedirectedTo);

Assert.IsNotNull(contactController.Flash["contact"]);
Assert.IsNotNull(contactController.Flash["summary"]);
Assert.AreEqual(1, ((ErrorSummary) contactController.Flash["summary"]).ErrorsCount);
}

The important code is here in the helper methods:

private void SimulateOneValidationErrorFor(SmartDispatcherController controller, object instance)
{
controller.ValidationSummaryPerInstance.Add(instance, CreateDummyErrorSummaryWithOneError());
}

private ErrorSummary CreateDummyErrorSummaryWithOneError()
{
ErrorSummary errors = new ErrorSummary();
errors.RegisterErrorMessage("blah", "blah");

return errors;
}

The ValidationSummaryPerInstance dictionary is publicly

exposed for us to manipulate (which is not my preferred way to expose/manipulate

collections, since it breaks encapsulation).  But for now this will get us by. 

We can just add our own dummied up error summary to the controller so that when

the HasValidationError method is called, it will return true,

since the result is based on an inspection of this dictionary of error

summaries.

Getting something like this included in the BaseControllerTest would also be

a nice thing to have.

There may be a better way to simulate this, but this is what I came up with. 

Anyone else got any better ways of doing this?

 

From Windsor XML to Binsor In 4 Hours