Unit Testing Handlers Guide

Introduction

One of the best things you can do when you develop software applications is TEST!! I know nobody likes it, but hey, you need to do it. With the advocation of frameworks today, you get all these great tools to build your software applications, but how do you test your framework code. As far as I know, ColdBox has revolutionized this, since now you can unit test your event handlers with ease and do integration testing. The new version of 2.0.1 includes an Application Template with unit tests already created for you, so no more excuses. Version 2.0.2 introduces a base unit test component included in the ColdBox's extra directory and unit test suites. As of now, ColdBox supports integration unit testing of handlers and framework code via cfcUnit, http://cfunit.sourceforge.net/cfUnit and our favorite MXUnit.

Resources

What is Unit Testing?

In computer programming, unit testing is a procedure used to validate that individual units of source code are working properly. A unit is the smallest testable part of an application. In procedural programming a unit may be an individual program, function, procedure etc, while in object-oriented programming, the smallest unit is always a Class; which may be a base/super class, abstract class or derived/child class. Units are distinguished from modules in that modules are typically made up of units.

Wikipedia

ColdBox is ahead in the unit testing field of ANY Framework and provides a way to unit test your event handlers by using either cfcUnit or cfUnit or MXunit. ColdBox has a built in testing controller and unit testing setup, to make sure that you can even test relocations via the internal relocation methods.

How to set it up?

In this guide I will use the included code of the Application Template included in the ColdBox bundle as an example. You can place your tests wherever you want. The basic skeleton of the tests are the following:

|ApplicationTemplate
|--+test
    |--+ integration
	|--+ cfcunit
	  |--+ AllTests.cfc (Test Suite)
          |--+ GeneralTest.cfc
          |--+ MainTest.cfc
        |--+ mxunit
          |--+ AllTests.cfc (Test Suite)
          |--+ GeneralTest.cfc
          |--+ MainTest.cfc       
    |--+ mock
    |--+ unit

Inside of the test folder you get three placeholder folders for tests

  • integration : For testing event handlers
  • mock : For mock objects
  • unit : For unit testing your model objects

If you open any of the test cases, you will see that they extend from the ColdBox base testing classes that can be found in the directory: coldbox/system/extras/testing/. The base classes are:

  • baseMXUnitTest.cfc : Base MXUnit test case
  • baseTest.cfc : Base cfcUnit test case

They extend these classes in order to get the correct ColdBox testing framework setup and usable. Let's analyze one:

  1. I have an event handler named General.cfc that has two events: index and doSomething.
  2. I have a test case cfc that extends my coldbox.system.extras.testing.baseMXUnitTest and has two test methods on it testindex, testdoSomething and a setup method where you will place the application's mapping and configuration file location.

The Handler To Test

Below you will see the source of the General.cfc:

<cfcomponent name="General" extends="coldbox.system.eventhandler" output="false">

<cffunction name="index" access="public" returntype="void" output="false">
        <cfargument name="Event" type="coldbox.system.beans.requestContext">
        <!--- Do Your Logic Here to prepare a view --->
        <cfset Event.setValue("welcomeMessage","Welcome to ColdBox!")>  
        <!--- Set the View To Display, after Logic --->
        <cfset Event.setView("vwHello")>
</cffunction>
        
<cffunction name="doSomething" access="public" returntype="void" output="false">
        <cfargument name="Event" type="coldbox.system.beans.requestContext">
        <!--- Do Your Logic Here, call to models, etc.--->

        <!--- Set the next event to run, after Logic, this relocates the browser--->
        <cfset setNextEvent("General.dspSomething")>
</cffunction>

</cfcomponent>

Very simple stuff on the handler, just setting a value in the request collection and setting a view to render. The doSomething method has a relocation in the code via the setnextevent method, a test controller is created for you and you can test event relocations. So whenever a setnextevent is fired, the test controller will just create a setnextevent variable in the request collection, indicating that a relocation to another event was requested and the value of the relocation is saved. So now to the test case...

IMPORTANT Any relocation produced by the framework via the setNextEvent & setNextRoute methods will produce two variables in the request collection for you to verify relocations. So the variables created will be: setNextEvent or setNextRoute

Notes when Handler returns values

If you have written event handlers that actually return data, then you will have to get the values from the request collection. This is done in order to assert returned results from handlers when most likely this handler is called from the coldbox proxy. The following are the keys created for you in the request collection.

Key Description
cbox_handler_results The value returned from the event handler method. This key will NOT be created if the handler does not return any data.

Notes on using setnextEvent and setnextroute

The test controller for unit testing will intercept any relocations via setnextevent and setnextroute and place at most two variables in the request collection in order for you to assert. The following are the variables that this method will set for you:

Key Description
setnextevent The value of the event + query string for you to assert. If no query string is supplied, then only the event will be here.
setnextroute The value of the route you want to relocate to.
persistKeys A list of the persist keys that you sent in, in order for you to assert.
persistVarStruct The structure used for persistence
ssl The ssl argument value if used
baseURL The base url argument value if used

The Base Test Case

This cfc is used in order to correctly setup your test cases and to configure ColdBox for Unit Testing. This setup differs from version 2.0.1 as you now only modify two variables in your actual test case and not in the base test case. The base test is now static and below are the methods that you will inherit:

Method Description
setup() The setup method where the coldbox controller and virtual application gets created and configured.
getAppMapping() Getter for the application's mapping
setAppMapping() Setter for the application's mapping
getConfigMapping() Getter for the config.xml location
setConfigMapping() Setter for the config.xml location
getcontroller() Getter for the ColdBox controller.
getRequestContext() Getter for the current request collection object (event)
setupRequest() Set's up a new request for execution, called internally only
announceInterception() Announce an interception state with an intercept data
getInterceptor() Get a reference to a declared and cached interceptor in the application
execute() Execute an event to test
dump() and abort() CF Facades

You also inherit the following public properties that you can adjust to your liking:

  • this.PERSIST_FRAMEWORK = boolean (Default is true)

If this property is set to true then the unit testing framework will persist the coldbox controller in application scope. Once the test is done, the unit test framework will drop it from the application scope. Useful, when you need to test coldbox proxy elements or coldbox factory methods.

<cfset this.PERSIST_FRAMEWORK = false>

The base test case is based off either MXUnit or cfcUnit, so if you are using CFUnit, then change the extends attribute of the component to net.sourceforge.cfunit.framework.TestCase and you are ready to roll with CFUnit also.

So what do I need to know about this base test?

Well, that all your test cases need to extend this component. So you need to know what it does and how it does it, for you to understand how your test cases will work. This component basically sets up a virtual application for you to use without rendering anything and providing you with a way to test the entire application headlessly.

Optional Setup: Application Start and Request Start Handler

  • Execute the Application Start handler if needed. You will need to fill out the name of the Application Start Handler to be executed and uncomment it.
  • Execute the Request Start Handler if needed. You will need to fill out the name of the Request Start Handler to be executed and uncomment it.

ColdBox Factory usage and Application scope

If you are using the ColdBox factory within your application, your tests might fail because the factory is expecting the controller to exist in the application scope. Therefore, you will need to alter your component to have the PERSIST_FRAMEWORK public property set to true as we discussed above.

The test case: GeneralTest.cfc

So what do I modify and what do I do? You will need to modify the setup method created for you in the application template or in the generated application. Basically you need to set up the Application's Mapping, so it can find the handlers, etc. and the location of the coldbox.xml file to use. So you can even create a coldbox.xml file for unit testing, such as coldbox_testing.xml.

<cffunction name="setUp" returntype="void" access="private" output="false">
  <cfscript>
  //Setup ColdBox Mappings For this Test
  setAppMapping("/applications/coldbox/ApplicationTemplate");
  setConfigMapping(ExpandPath(instance.AppMapping & "/config/coldbox.xml.cfm"));
        
  //Call the super setup method to setup the app.
  super.setup();

  //EXECUTE THE APPLICATION START HANDLER: UNCOMMENT IF NEEDED AND FILL IT OUT.
  //getController().runEvent("main.onAppInit");

  //EXECUTE THE ON REQUEST START HANDLER: UNCOMMENT IF NEEDED AND FILL IT OUT
  //getController().runEvent("main.onRequestStart");
  </cfscript>
</cffunction>

The Functions To Call

setAppMapping() The application mapping relative from the web root or via CF Mappings
setConfigMapping The location of your application's ColdBox configuration file. (You can have one explicitly set for Unit Testing: config_tests.xml.cfm)
IMPORTANT NOTE: If you will be testing an application that resides in the root of your web server, then the path is basically the root /. If you set the application mapping to / the tests, they will fail. You will need to change the front slash to a back slash in order for the tests to work from the web root.
//Setup ColdBox Mapping For Web Root Tests
setAppMapping("\");

After you have set up these two variables you are ready to call the extended base case's setup method to create the controller and prepare for testing. You do this by calling the super.setup() method. After that method gets executed, you are ready for testing.

Full Test Case

You can see below the source code for the full test case for cfcunit

<cfcomponent name="GeneralTest" extends="baseTest" output="false">

<cffunction name="setUp" returntype="void" access="private" output="false">
  <cfscript>
  //Setup ColdBox Mappings For this Test
  setAppMapping("/applications/coldbox/ApplicationTemplate");
  setConfigMapping(ExpandPath(instance.AppMapping & "/config/config.xml.cfm"));
        
  //Call the super setup method to setup the app.
  super.setup();

  //EXECUTE THE APPLICATION START HANDLER: UNCOMMENT IF NEEDED AND FILL IT OUT.
  //getController().runEvent("main.onAppInit");

  //EXECUTE THE ON REQUEST START HANDLER: UNCOMMENT IF NEEDED AND FILL IT OUT
  //getController().runEvent("main.onRequestStart");
  </cfscript>
</cffunction>

<cffunction name="testindex" access="public" returntype="void" output="false">
  <cfscript>
  var event = "";
        
  //Place any variables on the form or URL scope to test the handler.
  //FORM.name = "luis"
  event = execute("General.index");
                        
  //Do your asserts below
  assertEqualsString("Welcome to ColdBox!", event.getValue("welcomeMessage",""), "Failed to assert welcome message");
                        
  </cfscript>
</cffunction>

<cffunction name="testdoSomething" access="public" returntype="void" output="false">
  <cfscript>
  var event = "";
                
  //Place any variables on the form or URL scope to test the handler.
  //FORM.name = "luis"
  event = execute("general.doSomething");
                        
  //Do your asserts below for setnextevent you can test for a setnextevent boolean flag
  assertEqualsString("general.dspHome", event.getValue("setnextevent",""), "Relocation Test");
                        
  </cfscript>
</cffunction>

</cfcomponent>

Here is the test case for mxunit:

<cfcomponent name="generalTest" extends="coldbox.system.extras.testing.baseMXUnitTest" output="false">
        
        <cfscript>
        //Uncomment the following if you dont' need the controller in application scope for testing.
        //this.PERSIST_FRAMEWORK = false;
        </cfscript>

        <cffunction name="setUp" returntype="void" access="public" output="false">
                <cfscript>
                //Setup ColdBox Mappings For this Test
                setAppMapping("/coldbox/ApplicationTemplate");
                setConfigMapping(ExpandPath(instance.AppMapping & "/config/coldbox.xml.cfm"));
                        
                //Call the super setup method to setup the app.
                super.setup();
                
                //EXECUTE THE APPLICATION START HANDLER: UNCOMMENT IF NEEDED AND FILL IT OUT.
                //getController().runEvent("main.onAppInit");

                //EXECUTE THE ON REQUEST START HANDLER: UNCOMMENT IF NEEDED AND FILL IT OUT
                //getController().runEvent("main.onRequestStart");
                </cfscript>
        </cffunction>
        
        <cffunction name="testindex" access="public" returntype="void" output="false">
                <cfscript>
                var event = "";
                
                //Place any variables on the form or URL scope to test the handler.
                //FORM.name = "luis"
                event = execute("general.index");
                
                debug(event.getCollection());
                
                //Do your asserts below
                assertEquals("Welcome to ColdBox!", event.getValue("welcomeMessage",""), "Failed to assert welcome message");
                        
                </cfscript>
        </cffunction>
        
        <cffunction name="testdoSomething" access="public" returntype="void" output="false">
                <cfscript>
                var event = "";
                
                //Place any variables on the form or URL scope to test the handler.
                //FORM.name = "luis"
                event = execute("general.doSomething");
                
                debug(event.getCollection());
                        
                //Do your asserts below for setnextevent you can test for a setnextevent boolean flag
                assertEquals("general.index", event.getValue("setnextevent",""), "Relocation Test");
                        
                </cfscript>
        </cffunction>
        
</cfcomponent>

Once the unit test framework creates this test case, it will execute the setUp and prepare the ColdBox instance for execution. Then we can concentrate on the testindex method. Please note that what you are simulating/testing here is actual requests to your application and therefore those request need data. So how do I submit something or send something via the url, well just use the FORM or URL scopes. That easy. So if I wanted to set two variables before I execute the handler, which the handler is expecting, I can do the following:

form.fname = "luis";
form.lname = "majano";
url.mytest = "value";

This prepares the form or url if needed that will be grabbed by the testing controller and create the request collection out of them. The next step you see is the execute() method and the name of the event to test: General.index. This method will take your FORM/URL variables, create them in the request context, execute the handler and come back for your testing with the event object (coldbox.system.beans.requestContext). You can then do any type of asserts on the request collection if needed. So in summary, the execute method, simulates a full browser hit to an event and then you can test if the event did what is was supposed to do. You can even test if the handler relocated by asserting for the setnextevent flag.

Important Caveats with Relative Paths

When doing unit testing of applications you will find out that sometimes it will fail because it cannot find some of the relative file declarations you have done in your coldbox.xml.cfm. This happens because the unit testing framework is the one being executed and ColdBox is just simulating your application. So the best practice is to have a coldbox.xml for unit testing that is separate and has some absolute mappings. However, start testing with the relative application pathing and move to absolute if problems occur.

Important Caveats with FORM elements

If you are doing your testing via the Eclipse plugins or remotely, you might encounter problems when dealing with FORM variables. Below is an explanation on how to test these thanks to Marc Esher of the MXUnit Project.

In the quick-generated tests that come with coldbox, for example in generalTest.cfc, you'll see this:

<cffunction name="testonException" access="public" returntype="void" output="false">
   <cfscript>
   //You need to create an exception bean first and place it on the
request context FIRST as a setup.
   var exceptionBean =
CreateObject("component","coldbox.system.beans.exceptionBean");
   var event = "";

   //Initialize an exception
   exceptionBean.init(erroStruct=structnew(), extramessage="My unit
test exception", extraInfo="Any extra info, simple or complex");
   //attach it to request...
   form.exceptionBean = exceptionBean


   //TEST EVENT EXECUTION
   event = execute("main.onException");

   //Do your asserts HERE

   </cfscript>
</cffunction>

This will work fine if you run the test in the browser, either via generalTest.cfc?method=runTestRemote or via a directoryTestSuite in a .cfm file. However, this will fail if you run the test in the plugin. You'll get an error that exceptionBean is not defined in form.

The reason is that the plugin interacts with CF via webservice (SOAP) calls, and thus the form and url scopes are not present... they're not defined at all. So, I wanted to show the super simple way of getting around this in coldbox.

instead of attaching data to the form scope, simply add it directly to the request context itself, which is conveniently made available in the base test. You do it like so:

var event = getRequestContext();

event.setValue("exceptionBean",exceptionBean);

In essence, this is doing the same thing you'd do when you attach the data to the form scope, because under the hood coldbox just merges the form and url scope into the requestcollection anyway.

So, for those of you using mxunit and coldbox, and who are attempting to test your event handlers, that's how to do it in the plugin.

ColdBox Test Suites

So now that you completed your incredible unit test, you can also create testing suites. The Application Template comes with a pre-coded testing suite to cover the two test cases: generalTest.cfc and mainTest.cfc. So below you can see the code for the test suite for cfcunit:

<cfcomponent displayname="AllTests" output="false" hint="Test suite for test cases.">  
        
        <cffunction name="suite" returntype="org.cfcunit.framework.Test" access="public" output="false">  
                <cfset var suite = CreateObject("component", "org.cfcunit.framework.TestSuite").init("Test Suite")>  
                
                <!--- Add the test cases --->
                <cfset suite.addTestSuite(CreateObject("component", "cases.generalTest"))>
                <cfset suite.addTestSuite(CreateObject("component", "cases.mainTest"))>
                
                <cfreturn suite/>  
        </cffunction> 

</cfcomponent>

Here is the test suite for mxunit:

<cfscript>
testSuite = CreateObject("component","mxunit.framework.TestSuite").TestSuite();
//Add all runnable methods in MyComponentTest  
testSuite.addAll("generalTest");  
testSuite.addAll("mainTest");

results = testSuite.run();  
writeOutput(results.getResultsOutput('html'));
</cfscript>

All this does is add the two test cases we created to the suite. We can then test all of them at once via cfcUnit or test the entire directory via mxunit.

Eclipse integration via mxunit

MXUnit already includes an incredible eclipse plugin so you can test your components remotely. You can find more about it by following the following links:

Eclipse Integration via cfcUnit CFEclipse Facade

Thanks to Sean Corfield and Mark Drew's CFEclipse Project, you can also test your unit tests via CFEclipse. I am assuming you have Eclipse, CFEclipse and cfcUnit installed in your machine. All you need to do is the following:

Step 1: CFEclipse cfUnit Plugin

Make sure you install the cfUnit Plugin from the CFEclipse's distribution site. I will not delve here, there are plenty of tutorials on how to do this. For more information visit http://www.cfeclipse.org/index.cfm?event=page&page=download

Step 2: Download Sean's CFEclipse Facade

To install, first read Sean's Post at http://corfield.org/blog/index.cfm/do/blog.entry/entry/cfcUnit_Facade_for_CFUnit_Plugin, all you do is place the cfc in a web accessible location. In my case, I just place it inside of my cfcUnit directory.

Step 3: Configure the CFUnit plugin via Eclipse

To do this, go to Window > Preferences. Once the preference panel open, expand the CFEclipse option on the left and click on the CFUnit option. You will see the following screen, then just fill out the URL of the facade location:

Step 4: Open the CFUnit View

Click on the CFUnit icon or goto Window > Show View > Other... or via the view icon if you have one.

You can then see a listing of all the available views, look for the CFUnit view in the CFML folder:

You then press ok, and you will get the main CFUnit screen:

Step 5: Type the path of the test case or suite test

You can then type the path of the test case or suite to test, in my case, I type the path of my test suite: AllTests and then click on the execute button that looks like a play button. I can then see my results:

Conclusion

As you can see, ColdBox makes it really easy for you to test your handlers and your application. So no more excuses, get in to the best practice of unit testing and now unit test your application code. Enjoy.