Auto Generating Unit Tests With cfcGenerator

Earlier this week Peter Bell asked if anyone was doing anything in the way of auto generating unit tests. Well, I've been using Brian Rinaldi's cfcGenerator for a project at work and early on I realized that having some unit tests for all the components I was creating with the tool would probably be a good thing. If you haven't seen Brian's tool it is very cool. Out of the box it generates Bean, DAO, Gateway and Service objects for a database table as well as a ColdSpring snippet which you can use in your bean definition XML file. The really great thing about this tool is that it is very easy to extend and customize. So knowing I really should be building unit tests and seeing how easy it was to work with the cfcGenerator I decided to download cfcUnit spent a couple of hours hacking together a way to generate some unit tests using the cfcGenerator.

Before we go any further I have to warn you that this was (is) my first attempt at using any xUnit framework. Also, this is my first pass at auto generating unit tests using cfcGenerator. As you will see the generated tests are pretty basic, but I'm hoping to expand on this sometime in the near future.

Anyway, on to the code. (Note, I'm working on MS SQL Server so this code is specific to that database, but it should be easy enough to port what I outline here over to the other databases that the tool supports.) The cfcGenerator works by first building and XML document of table metadata which it then transforms into the various components using XSLT. The first thing I needed to do was add some test values to this metadata based on the column type. Generating the test values could be done in the XSL templates with and <xsl:choose> but I thought generating the values in the metadata would make it easier to build the XSL templates. To generate test values I added the following method to the mssql.cfc component:

<cffunction name="getTestValue" hint="I translate the MSSQL data type into a test value" output="false" returntype="string">
   <cfargument name="typeName" hint="I am the type name to translate" required="yes" type="string" />

   <cfswitch expression="#arguments.typeName#">
      <cfcase value="bigint">
         <cfreturn "123456789" />
      </cfcase>
      <cfcase value="binary">
         <cfreturn "binary" />
      </cfcase>
      <cfcase value="bit">
         <cfreturn "1" />
      </cfcase>
      <cfcase value="char">
         <cfreturn "'a'" />
      </cfcase>
      <cfcase value="datetime">
         <cfreturn "testTime" />
      </cfcase>
      <cfcase value="decimal">
         <cfreturn "1.1" />
      </cfcase>
      <cfcase value="float">
         <cfreturn "1.1" />
      </cfcase>
      <cfcase value="image">
         <cfreturn "image" />
      </cfcase>
      <cfcase value="int">
         <cfreturn "1234" />
      </cfcase>
      <cfcase value="money">
         <cfreturn "'$1.23'" />
      </cfcase>
      <cfcase value="nchar">
         <cfreturn "'n'" />
      </cfcase>
      <cfcase value="ntext">
         <cfreturn "'text_string_ntext'" />
      </cfcase>
      <cfcase value="numeric">
         <cfreturn "12345" />
      </cfcase>
      <cfcase value="nvarchar">
         <cfreturn "'text_string_nvarchar'" />
      </cfcase>
      <cfcase value="real">
         <cfreturn "'123456'" />
      </cfcase>
      <cfcase value="smalldatetime">
         <cfreturn "testTime" />
      </cfcase>
      <cfcase value="smallint">
         <cfreturn "123" />
      </cfcase>
      <cfcase value="smallmoney">
         <cfreturn "'$0.01'" />
      </cfcase>
      <cfcase value="text">
         <cfreturn "'text_string_text'" />
      </cfcase>
      <cfcase value="timestamp">
         <cfreturn "testTime" />
      </cfcase>
      <cfcase value="tinyint">
         <cfreturn "12" />
      </cfcase>
      <cfcase value="uniqueidentifier">
         <cfreturn "'#Insert("-", CreateUUID(), 23)#'" />
      </cfcase>
      <cfcase value="varbinary">
         <cfreturn "varbinary" />
      </cfcase>
      <cfcase value="varchar">
         <cfreturn "'text_string_varchar'" />
      </cfcase>
   </cfswitch>
</cffunction>

This method returns very simple values based on the column type. One thing I wasn't sure of was how to create test image and binary values so I just return stings for these cases. Next I modified getTableXML method to include a test value for each column:

<cffunction name="getTableXML" access="public" output="false" returntype="xml">
      <cfset var xmlTable = "" />
   <!--- convert the table data into an xml format --->
   <!--- added listfirst to the sql_type because identity is sometimes appended --->
   <cfxml variable="xmlTable">
   <cfoutput>
   <root>
      <bean name="#listLast(variables.componentPath,'.')#" path="#variables.componentPath#">
         <dbtable name="#variables.table#">
         <cfloop query="variables.tableMetadata">
            <column name="#variables.tableMetadata.column_name#"
                  type="<cfif variables.tableMetadata.type_name EQ 'char' AND variables.tableMetadata.length EQ 35 AND listFind(variables.primaryKeyList,variables.tableMetadata.column_name)>uuid<cfelse>#translateDataType(listFirst(variables.tableMetadata.type_name,"
"))#</cfif>"
                  cfSqlType="#translateCfSqlType(listFirst(variables.tableMetadata.type_name," "))#"
                  required="#yesNoFormat(variables.tableMetadata.nullable-1)#"
                  length="#variables.tableMetadata.length#"
                  primaryKey="#yesNoFormat(listFind(variables.primaryKeyList,variables.tableMetadata.column_name))#"
                  identity="#variables.tableMetadata.identity#"
                  testValue="#getTestValue(listFirst(variables.tableMetadata.type_name," "))#"/>
         </cfloop>
         </dbtable>
      </bean>
   </root>
   </cfoutput>
   </cfxml>      
   <cfreturn xmlTable />      
</cffunction>

With these test values available generating unit tests was pretty straight forward. For this post I'll walk through the steps necessary to get cfcGenerator to create a cfcUnit test case for the bean. First I created a new project for my custom XSL templates under cfcGenerator/xsl/projects/cfcunit. (Note the name of the cfcunit directory should match the name of datasource you will be generating the components for.) Next I copied the contents of the prototype folder (cfcGenerator/xsl/projects/prototype) into my new cfcunit project folder. I created a new beanTest.xsl file in the cfcunit project file which generates the base of my bean test component. That file looks like this:

<?xml version="1.0" encoding="UTF-8"?>

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
   <xsl:output method="text" indent="no" />
      
      <xsl:variable name="lcletters">abcdefghijklmnopqrstuvwxyz</xsl:variable>
      <xsl:variable name="ucletters">ABCDEFGHIJKLMNOPQRSTUVWXYZ</xsl:variable>
   
      <xsl:template match="/">
&lt;cfcomponent displayname="<xsl:value-of select="//bean/@name"/>Test" extends="org.cfcunit.framework.TestCase" output="false" hint="This tests the <xsl:value-of select="//bean/@name"/> component."&gt;    
   &lt;cfset variables.<xsl:value-of select="//bean/@name"/> = "" /&gt;       
   &lt;cffunction name="setUp" access="private" returntype="void" output="false"&gt;
      &lt;cfset variables.<xsl:value-of select="//bean/@name"/> = CreateObject("component", "<xsl:value-of select="//bean/@path"/>") /&gt;     &lt;cfset variables.testTime = Now() /&gt;
   &lt;/cffunction&gt;
         
   <!-- custom code -->
   
&lt;/cfcomponent&gt;
      </xsl:template>
   
      <xsl:template name="firstupper">
         <xsl:param name="toconvert" />
         <xsl:if test="string-length($toconvert) > 0">
            <xsl:variable name="f" select="substring($toconvert, 1, 1)" />
            <xsl:variable name="s" select="substring($toconvert, 2)" />
            <xsl:value-of select="translate($f,$lcletters,$ucletters)" />
            <xsl:value-of select="$s" />
         </xsl:if>
      </xsl:template>
   
</xsl:stylesheet>

Next I created a beanTest (cfcGenerator/xsl/projects/cfcunit/beanTest) to hold the additional pieces of my beanTest.xsl. I put three files in the folder.

testAccessorMutator.xsl

<!-- test individual accessors/mutators -->         
&lt;!--- test accessors/mutators ---&gt;         
<xsl:for-each select="root/bean/dbtable/column">
&lt;!--- <xsl:value-of select="@name" /> ---&gt;      
&lt;cffunction name="testSet<xsl:call-template name="firstupper"><xsl:with-param name="toconvert" select="@name" /></xsl:call-template>" access="public" returntype="void" output="false"&gt;
   &lt;cfset var testTime = variables.testTime /&gt;
   &lt;cfset variables.<xsl:value-of select="//bean/@name"/>.set<xsl:call-template name="firstupper"><xsl:with-param name="toconvert" select="@name" /></xsl:call-template>(<xsl:value-of select="@testValue" />) /&gt; &lt;/cffunction&gt;      
&lt;cffunction name="testGet<xsl:call-template name="firstupper"><xsl:with-param name="toconvert" select="@name" /></xsl:call-template>" access="public" returntype="void" output="false"&gt;
   &lt;cfset var testTime = variables.testTime /&gt;
   &lt;cfset variables.<xsl:value-of select="//bean/@name"/>.get<xsl:call-template name="firstupper"><xsl:with-param name="toconvert" select="@name" /></xsl:call-template>() /&gt;    <xsl:choose>
      <xsl:when test="@type = 'numeric'">&lt;cfset assertEqualsNumber(<xsl:value-of select="@testValue" />,variables.<xsl:value-of select="//bean/@name"/>.get<xsl:call-template name="firstupper"><xsl:with-param name="toconvert" select="@name" /></xsl:call-template>()) /&gt;</xsl:when>       <xsl:when test="@type = 'date'">&lt;cfset assertEqualsDate(<xsl:value-of select="@testValue" />,variables.<xsl:value-of select="//bean/@name"/>.get<xsl:call-template name="firstupper"><xsl:with-param name="toconvert" select="@name" /></xsl:call-template>(),"s") /&gt;</xsl:when>       <xsl:otherwise>&lt;cfset assertEqualsString(<xsl:value-of select="@testValue" />,variables.<xsl:value-of select="//bean/@name"/>.get<xsl:call-template name="firstupper"><xsl:with-param name="toconvert" select="@name" /></xsl:call-template>()) /&gt;</xsl:otherwise>    </xsl:choose>
&lt;/cffunction&gt;         
</xsl:for-each>

testInit.xsl

<!-- test init -->
&lt;cffunction name="testInit" access="public" returntype="void" output="false"&gt;
   &lt;cfset var testTime = Now() /&gt;

   &lt;!--- initialize the bean ---&gt;
   &lt;cfset variables.<xsl:value-of select="//bean/@name"/>.init(<xsl:for-each select="root/bean/dbtable/column"><xsl:value-of select="@name" />=<xsl:value-of select="@testValue" /><xsl:if test="position() != last()">, </xsl:if></xsl:for-each>) /&gt;
   &lt;!--- test that each value was initialized correctly ---&gt;
   <xsl:for-each select="root/bean/dbtable/column">
   &lt;!--- <xsl:value-of select="@name" /> ---&gt;
   <xsl:choose>
      <xsl:when test="@type = 'numeric'">&lt;cfset assertEqualsNumber(<xsl:value-of select="@testValue" />,variables.<xsl:value-of select="//bean/@name"/>.get<xsl:call-template name="firstupper"><xsl:with-param name="toconvert" select="@name" /></xsl:call-template>()) /&gt;</xsl:when>       <xsl:when test="@type = 'date'">&lt;cfset assertEqualsDate(<xsl:value-of select="@testValue" />,variables.<xsl:value-of select="//bean/@name"/>.get<xsl:call-template name="firstupper"><xsl:with-param name="toconvert" select="@name" /></xsl:call-template>(),"s") /&gt;</xsl:when>       <xsl:otherwise>&lt;cfset assertEqualsString(<xsl:value-of select="@testValue" />,variables.<xsl:value-of select="//bean/@name"/>.get<xsl:call-template name="firstupper"><xsl:with-param name="toconvert" select="@name" /></xsl:call-template>()) /&gt;</xsl:otherwise>    </xsl:choose>
   </xsl:for-each>   

&lt;/cffunction&gt;

and testMemento.xsl

<!-- test memento -->      
&lt;cffunction name="testMemento" access="public" returntype="void" output="false"&gt;

   &lt;cfset var testTime = Now() /&gt;
   &lt;cfset var inputStruct = StructNew() /&gt;
   &lt;cfset var returnStruct = StructNew() /&gt;

   &lt;!--- set up input structure ---&gt;
   <xsl:for-each select="root/bean/dbtable/column">&lt;!--- <xsl:value-of select="@name" /> ---&gt;
   &lt;cfset inputStruct.<xsl:value-of select="@name" /> = <xsl:value-of select="@testValue" /> /&gt;
   </xsl:for-each>   

   &lt;!--- set up extra properties , these should not be set or returned ---&gt;
   &lt;cfset inputStruct.extraTextProperty = "Testing Bad Property" /&gt;
   &lt;cfset inputStruct.extraNumericProperty = 12345 /&gt;

   &lt;cfset variables.<xsl:value-of select="//bean/@name"/>.setMemento(inputStruct) /&gt;    &lt;cfset returnStruct = variables.<xsl:value-of select="//bean/@name"/>.getMemento() /&gt;
   &lt;!--- now read back properties ---&gt;
   <xsl:for-each select="root/bean/dbtable/column">
   &lt;!--- <xsl:value-of select="@name" /> ---&gt;
   <xsl:choose>
      <xsl:when test="@type = 'numeric'">&lt;cfset assertEqualsNumber(<xsl:value-of select="@testValue" />,returnStruct.<xsl:value-of select="@name" />) /&gt;</xsl:when>
      <xsl:when test="@type = 'date'">&lt;cfset assertEqualsDate(<xsl:value-of select="@testValue" />,returnStruct.<xsl:value-of select="@name" />,"s") /&gt;</xsl:when>
      <xsl:otherwise>&lt;cfset assertEqualsString(<xsl:value-of select="@testValue" />,returnStruct.<xsl:value-of select="@name" />) /&gt;</xsl:otherwise>
   </xsl:choose>
   </xsl:for-each>

   &lt;!--- test to see if the extra properties were returned ---&gt;
   &lt;cfif StructKeyExists(returnStruct,"extraTextProperty")&gt;
      &lt;cfset fail("Extra text bean property returned.") /&gt;
   &lt;/cfif&gt;   

   &lt;cfif StructKeyExists(returnStruct,"extraNumericProperty")&gt;
      &lt;cfset fail("Extra numeric bean property returned.") /&gt;
   &lt;/cfif&gt;         

&lt;/cffunction&gt;

I then updated my yac.xml (cfcGenerator/xsl/projects/cfcunit/yac.xml) to include these new templates.

<?xml version="1.0" encoding="utf-8"?>
<!-- yet another config -->
<generator>
   <bean>
      <i nclude file="setMemento.xsl" />
      <i nclude file="validate.xsl" />
      <i nclude file="getset.xsl" />
      <i nclude file="dump.xsl" />
   </bean>
   <beanTest>
      <i nclude file="testAccessorMutator.xsl" />
      <i nclude file="testInit.xsl" />
      <i nclude file="testMemento.xsl" />
   </beanTest>
   <dao>
      <i nclude file="create.xsl" />
      <i nclude file="read.xsl" />
      <i nclude file="update.xsl" />
      <i nclude file="delete.xsl" />
      <i nclude file="exists.xsl" />
      <i nclude file="save.xsl" />
      <i nclude file="queryRowToStruct.xsl" />
   </dao>
   <gateway>
      <i nclude file="getByAttributes.xsl" />
   </gateway>
   <service>
      <i nclude file="getBean.xsl" />
      <i nclude file="getBeansQuery.xsl" />
      <i nclude file="saveBean.xsl" />
      <i nclude file="deleteBean.xsl" />
   </service>
   <to />
   <coldspring />
</generator>

Now when I run the cfcGenerator I have an extra textarea with a code for a cfcUnit test case which exercises my bean component. Here is a sample of what that code ends up looking like. This test was against a table with four columns: objectID, objectName, objectDescription, and seq.

<cfcomponent displayname="MyTestObjectTest" extends="org.cfcunit.framework.TestCase" output="false" hint="This tests the MyTestObject component.">
   
   <cfset variables.MyTestObject = "" />
      
   <cffunction name="setUp" access="private" returntype="void" output="false">
      <cfset variables.MyTestObject = CreateObject("component", "MyTestObject") />
    <cfset variables.testTime = Now() />
   </cffunction>
         
            
   <!--- test accessors/mutators --->         
   
   <!--- objectID --->      
   <cffunction name="testSetObjectID" access="public" returntype="void" output="false">
      <cfset var testTime = variables.testTime />
      <cfset variables.MyTestObject.setObjectID('F3B602D7-9EBD-BB38-FDCB-A5C757B0D98B') />
   </cffunction>      
   <cffunction name="testGetObjectID" access="public" returntype="void" output="false">
      <cfset var testTime = variables.testTime />
      <cfset variables.MyTestObject.getObjectID() />
      <cfset assertEqualsString('F3B602D7-9EBD-BB38-FDCB-A5C757B0D98B',variables.MyTestObject.getObjectID()) />
   </cffunction>         
   
   <!--- objectName --->      
   <cffunction name="testSetObjectName" access="public" returntype="void" output="false">
      <cfset var testTime = variables.testTime />
      <cfset variables.MyTestObject.setObjectName('text_string_nvarchar') />
   </cffunction>      
   <cffunction name="testGetObjectName" access="public" returntype="void" output="false">
      <cfset var testTime = variables.testTime />
      <cfset variables.MyTestObject.getObjectName() />
      <cfset assertEqualsString('text_string_nvarchar',variables.MyTestObject.getObjectName()) />
   </cffunction>         
   
   <!--- objectDescription --->      
   <cffunction name="testSetObjectDescription" access="public" returntype="void" output="false">
      <cfset var testTime = variables.testTime />
      <cfset variables.MyTestObject.setObjectDescription('text_string_ntext') />
   </cffunction>      
   <cffunction name="testGetObjectDescription" access="public" returntype="void" output="false">
      <cfset var testTime = variables.testTime />
      <cfset variables.MyTestObject.getObjectDescription() />
      <cfset assertEqualsString('text_string_ntext',variables.MyTestObject.getObjectDescription()) />
   </cffunction>         
   
   <!--- seq --->      
   <cffunction name="testSetSeq" access="public" returntype="void" output="false">
      <cfset var testTime = variables.testTime />
      <cfset variables.MyTestObject.setSeq(1234) />
   </cffunction>      
   <cffunction name="testGetSeq" access="public" returntype="void" output="false">
      <cfset var testTime = variables.testTime />
      <cfset variables.MyTestObject.getSeq() />
      <cfset assertEqualsNumber(1234,variables.MyTestObject.getSeq()) />
   </cffunction>         
   


   <cffunction name="testInit" access="public" returntype="void" output="false">
      <cfset var testTime = Now() />
         
      <!--- initialize the bean --->
      <cfset variables.MyTestObject.init(objectID='F3B602D7-9EBD-BB38-FDCB-A5C757B0D98B', objectName='text_string_nvarchar', objectDescription='text_string_ntext', seq=1234) />
      
      <!--- test that each value was initialized correctly --->
      
      <!--- objectID --->
      <cfset assertEqualsString('F3B602D7-9EBD-BB38-FDCB-A5C757B0D98B',variables.MyTestObject.getObjectID()) />
      <!--- objectName --->
      <cfset assertEqualsString('text_string_nvarchar',variables.MyTestObject.getObjectName()) />
      <!--- objectDescription --->
      <cfset assertEqualsString('text_string_ntext',variables.MyTestObject.getObjectDescription()) />
      <!--- seq --->
      <cfset assertEqualsNumber(1234,variables.MyTestObject.getSeq()) />   

   </cffunction>   

      
   <cffunction name="testMemento" access="public" returntype="void" output="false">
      
      <cfset var testTime = Now() />
      <cfset var inputStruct = StructNew() />
      <cfset var returnStruct = StructNew() />
      
      <!--- set up input structure --->
      <!--- objectID --->
      <cfset inputStruct.objectID = 'F3B602D7-9EBD-BB38-FDCB-A5C757B0D98B' />
      <!--- objectName --->
      <cfset inputStruct.objectName = 'text_string_nvarchar' />
      <!--- objectDescription --->
      <cfset inputStruct.objectDescription = 'text_string_ntext' />
      <!--- seq --->
      <cfset inputStruct.seq = 1234 />
         
      
      <!--- set up extra properties, these should not be set or returned --->
      <cfset inputStruct.extraTextProperty = "Testing Bad Property" />
      <cfset inputStruct.extraNumericProperty = 12345 />
      
      <cfset variables.MyTestObject.setMemento(inputStruct) />
      <cfset returnStruct = variables.MyTestObject.getMemento() />
      
      <!--- now read back properties --->
      
      <!--- objectID --->
      <cfset assertEqualsString('F3B602D7-9EBD-BB38-FDCB-A5C757B0D98B',returnStruct.objectID) />
      <!--- objectName --->
      <cfset assertEqualsString('text_string_nvarchar',returnStruct.objectName) />
      <!--- objectDescription --->
      <cfset assertEqualsString('text_string_ntext',returnStruct.objectDescription) />
      <!--- seq --->
      <cfset assertEqualsNumber(1234,returnStruct.seq) />
            
      <!--- test to see if the extra properties were returned --->
      <cfif StructKeyExists(returnStruct,"extraTextProperty")>
         <cfset fail("Extra text bean property returned.") />
      </cfif>   
      
      <cfif StructKeyExists(returnStruct,"extraNumericProperty")>
         <cfset fail("Extra numeric bean property returned.") />
      </cfif>         

   </cffunction>
   
</cfcomponent>

Obviously this is a fairly simple test case. I don't really test to the limits and, with the exception of the memento test, I don't really test for any failures. I'm also not exercising the bean's validation method. I'd really like to generate a few test values for each column in the table metadata and use those to create more robust tests, but for now I'm working with these basic tests. I do have to say that even these simple tests have saved me an enormous amount of time by helping me catch quite a few errors.

Anyway, that is all for now. I'll try to post some samples of my DAO and Gateway tests soon. If anyone has any suggestions or improvements please let me know.

Comments
Dan Wilson's Gravatar Great first cut Nathan. Thanks for sharing...
# Posted By Dan Wilson | 11/17/06 9:50 AM
Brian Rinaldi's Gravatar Nice work! Perhaps we can refine this to be included into a future version of the generator if you wouldn't mind. Hopefully we can talk and explore this further.

I got your link from Peter Bell's blog post and I commented there with some info on an upcoming significant release of the generator, hopefully to happen this weekend with some new features and a UI redone in Flex 2.
# Posted By Brian Rinaldi | 11/17/06 10:27 AM
Nathan's Gravatar Brian, I'm definitely interested in exploring this further. The generator has been a huge time saver for me and adding some sort of unit test generation could only make it that much better!

I'm looking forward to seeing the new Flex version.
# Posted By Nathan | 11/17/06 11:35 AM
Justin Treher's Gravatar Has anything else been done with CFUnit in CFCgenerator?
# Posted By Justin Treher | 6/1/07 10:51 AM
Nathan Mische's Gravatar Brian Rinaldi and I spoke about this a little bit at cfObjective. Apparently people are interested so I told him I would investigate it further. To be honest I'm kind of stuck on how to handle the database for DAOs and Gateways. Terrence Ryan posted about generated unit test for his Squidhead framework, so I've been meaning to check that out and see if I get any ideas.

The other problem I have is that the other CF developer on my team at work is leaving. His last day is today so, as much as I want to, I'm probably not going to have a lot of time to work on this over the next month or so.
# Posted By Nathan Mische | 6/1/07 11:40 AM
Justin Treher's Gravatar Thanks for the update. It would definitely be cool to see something done. Although, with as many generators and utilities coming out on a monthly basis, it may get lost. I don't know what the user base is for these vs. an ORM right now.
# Posted By Justin Treher | 6/1/07 2:08 PM
Brian Rinaldi's Gravatar I am happy to add that feature. I have not personally used any unit testing framework as of yet, which is why I asked Nathan to assist - though there are some other generators out there that are purely about generating unit test stuff (check out Brian Kotek's generator).

As for a user base, I have no exact number. I know that since I started tracking the generator has been download some 1500+ times from the two locations I have it available. It seems to have a pretty wide user base...but remember I work on this for free.

As for ORM, you are comparing apples and oranges. I mean the generator will actually even help you if you use transfer ORM since it generates transfer compatible files via one of the templates. Plus, no offense to Transfer, but ORM is not always the solution - sometimes hand-coding works nicely! Remember, the generator is about getting a head start, it is not about handing you off a complete application.
# Posted By Brian Rinaldi | 6/1/07 2:41 PM
Justin Treher's Gravatar Thanks Brian.

I'm actually not using an ORM as I don't want to become detached from the code, that's why I'm using your generator. I know that transfer needs the db info fed in, but I was thinking more of Reactor. I suppose I could look into an additional utility for unit testing, but I'm kind of a newb. I'll check out kotek's stuff.

So, in summary, if you get it in there, I'd use it :)

Happy Weekend!
# Posted By Justin Treher | 6/1/07 4:42 PM
Nathan Mische's Gravatar There is also Stubbie by Greg Stewart, which may be worth looking at: http://stubbie.riaforge.org/index.cfm
# Posted By Nathan Mische | 6/1/07 4:50 PM
BlogCFC was created by Raymond Camden. This blog is running version 5.8.001.