finishing the windsor mvc controller factory

This is the seventh in a series of posts illustrating building a custom Windsor registration process and I've almost completed the Windsor controller factory.

After testing out my controller with a quick integration into the test of my code base, I observed at runtime that if the controller cannot be found by the factory and it returns null that an error is thrown by the MVC framework:
"System.InvalidOperationException: The IControllerFactory 'Apollo.Infrastructure.Windsor.WindsorControllerFactory' did not return a controller for a controller named 'xxx'".

I now want to account for this and to communicate to the client the correct status for this http request and my specification for this is as follows:

[Observations]
public class when_an_unrecognised_controller_is_requested :
         observations_for_a_sut_with_a_contract<IControllerFactory, WindsorControllerFactory>
{
    private static string unrecognisedController;

    private context c = () => unrecognisedController = "NotRecognised";

    private because b = () => doing( ()=> sut.CreateController(null, unrecognisedController) );

    [Observation]
    public void should_fail_appropriately_as_a_http_error()
    {
        exception_thrown_by_the_sut.should_be_an_instance_of<HttpException>();
    }
}

Here, I am using the doing() method to delay the execution of my because action until the "exception_thrown_by_sut" is examined. This test fails because I am returning null when the controller type is not found, so I can implement this as follows:

public IController CreateController(RequestContext requestContext, string controllerName)
{
    var controllerType = GetControllerTypeFromLoadedAssemblies(controllerName);

    if(controllerType==null)
        throw new HttpException(0, string.Format("Could not find a 
                      controller for the request for {0}.", controllerName));

    return container.Resolve<IController>(controllerType.FullName.ToLower());
}

My test now passes, but I need one more observation to ensure this works as desired:

[Observations]
public class when_an_unrecognised_controller_is_requested : 
         observations_for_a_sut_with_a_contract<IControllerFactory, WindsorControllerFactory>
{
    private static string unrecognisedController;
    private static int theExpectedHttpStatusCode;

    private context c = () =>
                            {
                                unrecognisedController = "NotRecognised";
                                theExpectedHttpStatusCode = 404;
                            };

    private because b = () => doing( ()=> sut.CreateController(null, unrecognisedController) );

    [Observation]
    public void should_fail_appropriately_as_a_http_error()
    {
        exception_thrown_by_the_sut.should_be_an_instance_of<HttpException>();
    }

    [Observation]
    public void should_communicate_the_correct_http_status()
    {
        ((HttpException)exception_thrown_by_the_sut).GetHttpCode()
                          .should_be_equal_to(theExpectedHttpStatusCode);
    }
}

And this is easily implemented with a change to the status code when creating an instance of the HttpException:

public IController CreateController(RequestContext requestContext, string controllerName)
{
    var controllerType = GetControllerTypeFromLoadedAssemblies(controllerName);

    if(controllerType==null)
        throw new HttpException(404, string.Format("Could not find a 
                      controller for the request for {0}.", controllerName));

    return container.Resolve<IController>(controllerType.FullName.ToLower());
}

All that's left to do now on this class is to implement the ReleaseController method of the IControllerFactory interface so I write my specification for this first:

[Observations]
public class when_a_controller_is_released_from_the_windsor_controller_factory : 
       observations_for_a_sut_with_a_contract<IControllerFactory, WindsorControllerFactory>
{
    private static IWindsorContainer container;
    private static FakeController controller;

    private context c = () =>
    {
        container = the_dependency<IWindsorContainer>();
        controller = new FakeController();
    };

    private because b = () => sut.ReleaseController(controller);

    [Observation]
    public void should_release_the_controller_from_the_container()
    {
        container.was_told_to(c => c.Release(controller));
    }
}

In my specification I want to ensure that when the controller factory is asked to release the controller that the Windsor IOC container releases the transient controller instance and thus free it up for garbage collection - if I don't do this then the reference is held for the lifetime of the Windsor kernel.

The implementation is really straightforward:

public void ReleaseController(IController controller)
{ 
    container.Release(controller);   
}

I've now completed my illustration of test-driving the creation of a Windsor controller factory. In the next post in this series I'll move onto how I implemented this in the application.



3 comments:

  1. Jason HYland November 25, 2009 at 8:15 AM

    Hey Justin,

    Great series of posts. I'm still not entirely comfortable with the exception catching here. Seems to me that the "act" in the "assert" could lead to a test smell if/when additional observations are added (and what's with "doing" - I'd prefer a "catch_when" or something)

    Just out of interest, why not try/catch the exception into a static exception and assert against that?

    Regards, Jason

     
  2. Justin Davies November 25, 2009 at 2:14 PM

    Hi Jason, thanks for the feedback.
    I agree with you - it does seem a bit of a smell that the because, or Act, is actually performed during the assertion when the exception member is retrieved - the framework then processes the deferred action out of step. It certainly isn't intuitive.
    I'll have a think and post about an alternative approach.

     
  3. Jason HYland January 17, 2010 at 11:08 AM

    I know my observation my have rocked your world, but that's no reason to take your email adress offline :) the jrd email address is bouncing.