This document attempts to provide an overview of the installation and use of the ColdUnit automated unit testing framework.

Installation

Place the contents of the src directory into your ColdFusion server's CustomTags directory in a directory called coldunit. Edit the file coldunitVariables.cfm. The rootMappingPhysicalPath and rootMappingLogicalPath variables should be set to the physical and logical paths (see the Mappings section in ColdFusion administrator) of a mapping on your system that sits above the area where you want to place the application you want to test. Set fileSeparator to be the file separator character appropriate for your system (\ for Windows, / for Unix). Copy the code in the tests directory somewhere where your ColdFusion server can serve the CFML. Load the file allTests.cfm in the tests directory in a web browser. You should see something like:

....
Ran 4 tests: 4 passed, 0 failures, 0 errors in 0.382 seconds
Asserts: 2
array [empty]

Congratulations, you have successfully installed ColdUnit! Yes, we really should write more tests for the framework itself.

Usage Examples

Now say you are in a directory containing ColdFusion that you will want to test. Call this directory www-root. Below www-root, create a directory called tests. I will give two examples:

  1. writing simple tests for a custom tag
  2. writing a test for code that changes the database

Writing a simple test for a custom tag

Say we want to write a custom tag that capitalizes the first letter of every word in a string. In the tests subdirectory, create a directory called testCapitalize. Within testCapitalize, create a file called testCapitalizesSimplePhraseCorrectly.cfm with the following contents:

<CFMODULE template="../../capitalize.cfm" phrase="one two three" returnVar="capitalized">

<CFMODULE name="coldunit.assertEquals" expected="One Two Three" actual="#capitalized#" message="wrong capitalization">

In order to run our test, go back to the www-root directory and create a file called allTests.cfm with the following contents:

<CFMODULE name="coldunit.testRunner" root=".">
<html><body>
<CFMODULE name="coldunit.testResultsTextDisplay" testResults="#testResults#">
</body></html>

Now we will write capitalize.cfm. One way to do it is the following:

<CFPARAM name="attributes.phrase">
<CFPARAM name="attributes.returnVar">

<CFSET result = "">
<CFLOOP list="#attributes.phrase#" delimiters="#CHR(32)#" index="word">
  <CFSET result = result & UCase(Left(word, 1))>
  <CFSET result = result & Right(word, 1)>
  <CFSET result = result & " ">
</CFLOOP>

<CFSET "Caller.#attributes.returnVar#" = Trim(result)>

Point your browser at allTests.cfm in the www-root directory, and you should see something very similar (the number of seconds won't be the same) to the following:

.
Ran 1 tests: 0 passed, 1 failures, 0 errors in 0.186 seconds
Asserts : 1
array
1
struct
DETAIL [empty string]
MESSAGE Assertion failed: expected One, Two, Three but was One Two Tee, capitalization wrong
RESULT failure
TESTDIRECTORY /dev/example/tests/testCapitalize/
TESTFILENAME /dev/example/tests/testCapitalize/testCapitalizeSingleCharWord.cfm
TIME 0.124

Oops. I made a mistake, one I often make. It turns out the second argument of the built-in function, Right, is the number of characters to be taken from the right side of the first argument, not the starting point of the substring to be returned. I can fix the code like this:

  <CFSET result = result & Right(word, Len(word)-1)>

Now let's rerun my test and see if I did indeed fix the bug.

.
Ran 1 tests: 1 passed, 0 failures, 0 errors in 0.186 seconds
Asserts : 1
array [empty]

Looks good! But wait, we forgot a test, didn't we? We haven't really done any boundary condition testing. Let's write another test for capitalization of one-letter words. Place the following in the file testCapitalizeSingleCharWord.cfm in the testCapitalize directory:

<CFMODULE template="../../capitalize.cfm" phrase="a" returnVar="capitalized">

<CFMODULE name="coldunit.assertEquals" expected="A" actual="#capitalized#">

If we rerun allTests.cfm, we get the following result:

E.
Ran 2 tests: 1 passed, 0 failures, 1 errors in 5.642 seconds
Asserts : 1
array
1
struct
EXCEPTION
struct
DETAIL [empty string]
ERRORCODE [empty string]
MESSAGE Parameter 2 of function Right which is now 0 must be a positive integer
TAGCONTEXT
array
1
struct
COLUMN 0
ID CF_CFPAGE
LINE 7
RAW_TRACE at cfcapitalize2ecfm1556500062.runPage(/dev/example/capitalize.cfm:7)
TEMPLATE /dev/example/capitalize.cfm
TYPE CFML
2
struct
COLUMN 0
ID CFMODULE
LINE 1
RAW_TRACE at cftestCapitalizeSingleCharWord2ecfm296841007.runPage(/dev/example/tests/testCapitalize/testCapitalizeSingleCharWord.cfm:1)
TEMPLATE /dev/example/tests/testCapitalize/testCapitalizeSingleCharWord.cfm
TYPE CFML
3
struct
4
struct
5
struct
6
struct
7
struct
TYPE Expression
RESULT error
TESTDIRECTORY /dev/example/tests/testCapitalize/
TESTFILENAME /dev/example/tests/testCapitalize/testCapitalizeSingleCharWord.cfm
TIME 5.616

Looks like we did miss a test. What we see above is the typical ColdUnit output when an "Error" occurs. An error is defined as an exception that is not of the type cfunit.failure (which is generated by the framework whenever an assert fails). Notice that the TAGCONTEXT subelement of the EXCEPTION struct tells you exactly where the error occurred in your code. Also, I have collapsed the elements 3 through 7 of the TAGCONTEXT array for readability.

To fix the bug, we add the following code to capitalize.cfm and rerun the tests:

  <CFIF Len(word) GT 1>
    <CFSET result = result & Right(word, Len(word)-1)>
  </CFIF>

After rerunning the tests, we get:

..
Ran 2 tests: 2 passed, 0 failures, 0 errors in 0.194 seconds
Asserts : 2
array [empty]

You've now seen the three basic kinds of test results: success, (assertion) failure and error (the result of an exception being thrown).

Writing a test for a custom tag that alters the database

There are two main benefits of using allTests.cfm to run your tests.

  1. It will search recursively through all directories under the root attribute (in the example above root=".", i.e. the current directory) and find all tests. It considers a .cfm file a test if its filename begins with "test", ends in ".cfm", and if the file exists in a directory whose name begins with "test". This allows you to build hierarchical test suites easily, allowing you to run all of your tests, or only a given subset under a particular directory.
  2. More relevant to this section: the test runner used by allTests.cfm will surround the execution of each test in a transaction, and will rollback that transaction after each test. This way, any code that inserts/updates/deletes from tables in your database can safely be tested without altering the database.

This part is under construction.