An Architect's View

CFML, Clojure, Software Design, Frameworks and more...

An Architect's View

Testing a ColdBox Controller that Redirects

August 8, 2009 ·

I'm working on a ColdBox project right now for a client and, as some may have gathered from my occasional tweets, I've set up Hudson as a Continuous Integration server that pulls the latest code from git, restarts the test server instance, reloads the test database and automatically runs the MXUnit-based test suite, whenever someone commits files to the main git repository. We have unit tests for individual components and we have integration tests for the ColdBox event handlers. Luis has provided pretty good documentation for writing such tests but one of the challenges I faced in getting some of the integration tests working was that they redirected to a new event!Since you can't 'catch' a redirect, how can you test such handlers? ColdBox provides a relocate() method for handlers to perform redirects and it uses the <cflocation> tag. We can't change the core files but we might be able to change things at runtime to replace the framework's relocate() method with something more testable... If relocate() used <cfabort> with showerror=, then it could be caught as an exception and a test case could be written. So what we want is for the relocate() function to look something like this:
<cffunction name="fakeRelocate" access="private" returntype="void" output="false">
      <cfargument name="url"/>
      <cfabort showerror="Location: #arguments.url#"/>
   </cffunction>
The question is: how do we replace ColdBox's function? ColdBox provides a lot of 'events' in its lifecycle and you can write CFCs to intercept any of them. One of them is afterHandlerCreation so in theory you could write an interceptor for that which patched the runtime instance. Unfortunately, the ColdBox documentation didn't say how that event worked! I pinged Luis and he promptly updated the documentation and here is how you do it: Create an interceptor to do the patching - I called mine test.mock.interceptor.HandlerRelocatePatch:
<cfcomponent output="false">
   
   <cffunction name="afterHandlerCreation" access="public" returntype="void" output="false">
      <cfargument name="event" type="any"/>
      <cfargument name="interceptData" type="struct"/>
      <cfscript>
      // fake out the base handler relocate() method:       interceptData.oHandler._fakeInjector = variables.methodInjector;
      interceptData.oHandler._fakeInjector( "relocate", variables.fakeRelocate, true );
      </cfscript>
   </cffunction>
   
   <cffunction name="methodInjector" access="private" returntype="void" output="false">
      <cfargument name="methodName"/>
      <cfargument name="methodBody"/>
      <cfargument name="injectPublic" default="false"/>

      <cfset variables[arguments.methodName] = arguments.methodBody />
      <cfif arguments.injectPublic>
         <cfset this[arguments.methodName] = arguments.methodBody />
      </cfif>
   </cffunction>
   
   <cffunction name="fakeRelocate" access="private" returntype="void" output="false">
      <cfargument name="url"/>
      <cfabort showerror="Location: #arguments.url#"/>
   </cffunction>
   
</cfcomponent>
Once registered (see below), this is called after each event handler is created and it first injects a new public method called _fakeInjector (as a copy of the methodInjector code in the interceptor) and then it calls that new method to perform the injection of the private method relocate()! All you need to do is register the interceptor in your test case:
// fake out the base handler relocate() method:
      getController().getInterceptorService().registerInterceptor(
         interceptorObject = createObject("component", "test.mock.interceptor.HandlerRelocatePatch")
      );
Happy testing!

Tags: coldbox · coldfusion · tdd

6 responses

  • 1 Tyler Clendenin // Aug 8, 2009 at 11:43 AM

    Hudson sounds very interesting. And it seems that I and I'm sure by extension others would greatly benefit from any explanation of how you set up Hudson to do what you have described. Therefore I hear by request a multi-part series of blog posts on how you accomplished this magnificent environment =].
  • 2 Sean Corfield // Aug 8, 2009 at 12:32 PM

    @Tyler, I'll put it on my (long) list of things to blog about :)
  • 3 Sami Hoda // Aug 9, 2009 at 11:24 PM

    Sean,

    I'd benefit from a post on Hudson too. Look forward to it!
  • 4 John Allen // Aug 10, 2009 at 8:26 AM

    Would you recommend this type of approach for FW/1? Overriding or method injecting the cfabort on the FW/1 cfc?

    About two write a bunch of tests for some FW/1 controllers... this post is Timely. Thanks.
  • 5 Sean Corfield // Aug 10, 2009 at 11:20 AM

    It's much easier with FW/1. The controller is passed the framework at init() time so you can easily pass either a mock of FW/1 (you only need to support the methods the controller needs - easy if you use duck typing) or you can write a CFC that extends org.corfield.framework and just override redirect() to use cfabort showerror= and pass an instance of that extended CFC into the controller instead.

    So, yes, you'd want the try/catch around the controller test but you can use standard mocking techniques with FW/1.
  • 6 John Allen // Aug 10, 2009 at 1:15 PM

    Excellent! Didn't think about mocking the framework itself. That is a great idea.

    I've become a FW/1 convert. It truly rocks.

    Thanks Sean.