Test Driven Development with ColdFusion Part II : Building Unit Tests with CFUnit
by Robert Blackburn
In
part one of this article, we explored the advantages of Test Driven Development (TDD) and unit testing your code. Hopefully you are convinced that unit testing can be an invaluable aspect of your development process. In this part of the article, we will look at how CFUnit can help you accomplish this.
How Does CFUnit Help?
CFUnit is a unit testing utility designed for ColdFusion. It's based on the popular JUnit framework, a unit testing utility for Java. These types of testing frameworks are called xUnit, the original of which was designed by Kent Beck.
CFUnit is not required to unit test your ColdFusion code, but it is intended to help you write unit tests more quickly and easily. You are not required to use CFUnit for TDD; you can simply use it to automate testing of your code. But I think you will find that once you start using it, TDD will just naturally follow.
CFUnit uses ColdFusion Components (CFCs) to create test cases. Each test case is focused on testing a particular piece of an application (like a single file or CFC) and is comprised of multiple tests. To make a test case CFC, you need to extend the CFUnit TestCase class. The TestCase class will provide a suite of functions you can use to build your tests. The main ones you will be using are:
- assertTrue(message, condition): Asserts that the condition is true. If it isn't, the test fails and displays the given message.
- assertFalse(message, condition): Asserts that the condition is false. If it isn't, the test fails and displays the given message.
- assertEquals(message, expected, actual): Asserts that two objects are equal. If they are not, the test fails and displays the given message.
- assertSame(message, expected, actual): Asserts that two objects refer to the same object in memory. If they do not, the test fails and displays the given message.
- assertOutputs(template, expected, message): Asserts that a template (*.cfm file) or CF Module contains the expected output. If it doesn't, the test fails and displays the given message.
- fail(message): This will cause the test to fail and display the given message.
Installing CFUnit is very easy, and does not require any sort of administrative access. Simply download the framework from the CFUnit web site:
http://cfunit.sourceforge.net. Then unzip the contents to a mapped drive on your server or your site's web root. This will place everything you need on your site; no other software or IDE is needed to make use of CFUnit. More detailed instructions are included within the zip file and on the CFUnit web site.
Our Example Application
To show you how to write a unit test in CFUnit, we first need a very simple example application to test. Our example application will merely ask the user for two x/y coordinates, and then display the distance between the two. The application code will be provided in this article, and in a
downloadable file.
Our example application will have two files:
- index.cfm -- This is the initial page, which will have form fields for two sets of x/y coordinates. This page will also refresh itself and output the results.
- DistanceCalc.cfc -- This is a CFC, and will have one method for calculating the distance between two x/y coordinates.
Once you download the files, place them off of your web root in a folder named "TDDWithCF." Here is the code we will be testing:
Index.cfm:
<cfsilent>
<cfparam name="FORM.x1" default="0" type="numeric" />
<cfparam name="FORM.y1" default="0" type="numeric" />
<cfparam name="FORM.x2" default="0" type="numeric" />
<cfparam name="FORM.y2" default="0" type="numeric" />
<cfparam name="FORM.getresults" default="false" type="boolean" />
</cfsilent>
<cfoutput>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>
<title>CFUnit DistCalc</title>
</head>
<body>
<h1>Distance Calculator</h1>
<form action="index.cfm" method="POST">
<div>
<h3>Location 1</h3>
X: <input name="x1" value="#FORM.x1#" type="text" size="5"/> Y: <input name="y1" value="#FORM.y1#" type="text" size="5"/>
</div>
<div>
<h3>Location 2</h3>
X: <input name="x2" value="#FORM.x2#" type="text" size="5"/> Y: <input name="y2" value="#FORM.y2#" type="text" size="5"/>
</div>
<input name="getresults" value="true" type="hidden"/>
<input type="submit"/>
</form>
<cfif FORM.getresults>
<cfset VARIABLES.distcalc = CreateObject("component", "DistanceCalc").init() />
<strong>Distance: #VARIABLES.distcalc.getDistance( FORM.x1, FORM.y1, FORM.x2, FORM.y2 )#</strong>
</cfif>
</body>
</html>
</cfoutput>
DistanceCalc.CFC
<cfcomponent name="DistanceCalc" hint="This CFC will have one method for calculating the distance between two x/y coordinates.">
<cffunction name="init" access="public" output="false" returntype="DistanceCalc">
<cfreturn THIS />
</cffunction>
<cffunction name="getDistance" hint="Calculats the distance between two x/y coordinates." access="public" output="false" returntype="numeric">
<cfargument name="x1" type="numeric" required="true" />
<cfargument name="y1" type="numeric" required="true" />
<cfargument name="x2" type="numeric" required="true" />
<cfargument name="y2" type="numeric" required="true" />
<cfset var a = (ARGUMENTS.x2 - ARGUMENTS.x1)^2 />
<cfset var b = (ARGUMENTS.y2 - ARGUMENTS.y1)^2 />
<cfreturn Sqr( Abs(a + b) ) />
</cffunction>
</cfcomponent>
Unit Tests for DistanceCalc.cfc
We will start by building our DistanceCalc unit tests. With any unit test, I begin by trying to imagine all the different scenarios that might occur. The DistanceCalc CFC is a simple CFC with only one method named getDistance. I can think of three scenarios for this CFC and its method:
- The two sets of coordinates are valid, and the distance should be calculated.
- The two sets of coordinates are the same, and the distance is zero.
- One of more of the numbers supplied are negative. We do not want to allow negative numbers into our applications, so this should throw an error.
Based on this analysis, we will create our test case with three tests.
The Empty Test Case
First we must create an empty test case for our CFC. I usually place this in a "tests" subdirectory to my application, but you can place the tests anywhere you like. Here is the code for our empty test case:
TestDistanceCalc.cfc:
<cfcomponent extends="net.sourceforge.cfunit.framework.TestCase">
<cffunction name="testValid" returntype="void" access="public">
<cfset fail("Test not yet implemented") />
</cffunction>
<cffunction name="testSameLocations" returntype="void" access="public">
<cfset fail("Test not yet implemented") />
</cffunction>
<cffunction name="testNegatives" returntype="void" access="public">
<cfset fail("Test not yet implemented") />
</cffunction>
</cfcomponent>
In the code above, there are three test methods, one for each of the scenarios we wish to test. Each method has only one line,
<cfset fail("Test not yet implemented") />. If left that way, those three tests would always fail.
Give this a try. Save the test CFC in a "tests" folder within your application (for me, it's "TDDWithCF") and then browse to
http://localhost/TDDWithCF/tests/TestDistanceCalc.cfc?method=execute&verbose=1&html=1 . This will execute the test and show you the results, which will be three failures. Later we will look at how to more easily execute these tests.
(Note: This will automatically run any method whose name begins with "test". If you name your tests in some other way, it is still possible to execute them, but it requires a little more work. For more information, see the CFUnit web site).
The First Unit Test: The Coordinates are Valid
Now let's fill in the first unit test:
<cfcomponent extends="net.sourceforge.cfunit.framework.TestCase">
<cffunction name="setUp" returntype="void" access="public">
<cfset VARIABLES.distcalc = CreateObject("component", "TDDWithCF.DistanceCalc").init() />
</cffunction>
<cffunction name="testValid" returntype="void" access="public">
<cfset var actual = VARIABLES.distcalc.getDistance(1, 4, 4, 0) />
<cfset var expect = 5 />
<cfset assertEquals("Distance calculation failed", expect, actual) />
</cffunction>
<cffunction name="testSameLocations" returntype="void" access="public">
<cfset fail("Test not yet implemented") />
</cffunction>
<cffunction name="testNegatives" returntype="void" access="public">
<cfset fail("Test not yet implemented") />
</cffunction>
</cfcomponent>
Let's look at the new code. There are two changes I'd like you to notice. First, I added a setUp method, which will run before each test is executed. We can place code here that will be used by all of our tests, and executed prior to testing. In this scenario, all of our tests will need to create a DistanceCalc object. Therefore, I have placed that in the setUp method. There is also a tearDown method that you can use for any code you wish to execute at the end of each test in the test case.
Second, I've added three lines inside the testValid method, and removed the fail message. The first new line sets a local variable to the results of our getDistance method, passing in hard coded values. I did the math and determined that the values I passed should return a result of 5. The second line sets a local variable to the value I expect getDistance to return (5). And then, on the third line, I used the assertEquals method to assert that the value returned was what I expected. That's it! It's the equivalent of saying, "make sure that when I pass in these values, I get this response."
The Second Unit Test: The Coordinates are Identical
Now that we have a test to validate the results of normal values, let's create the test for when the locations passed are identical:
<cfcomponent extends="net.sourceforge.cfunit.framework.TestCase">
<cffunction name="setUp" returntype="void" access="public">
<cfset VARIABLES.distcalc = CreateObject("component", "TDDWithCF.DistanceCalc").init() />
</cffunction>
<cffunction name="testValid" returntype="void" access="public">
<cfset var actual = VARIABLES.distcalc.getDistance(1, 4, 4, 0) />
<cfset var expect = 5 />
<cfset assertEquals("Distance calculation failed", expect, actual) />
</cffunction>
<cffunction name="testSameLocations" returntype="void" access="public">
<cfset var actual = VARIABLES.distcalc.getDistance(2, 3, 2, 3) />
<cfset var expect = 0 />
<cfset assertEquals("Distance calculation failed", expect, actual) />
</cffunction>
<cffunction name="testNegatives" returntype="void" access="public">
<cfset fail("Test not yet implemented") />
</cffunction>
</cfcomponent>
In this test, I added three lines to the testSameLocation method. Those three lines are very similar to the content of the testValid method, except that I pass in different arguments and have a different expected value. If you refresh your browser, you will see that there is now only one failure remaining.
The Third Test: The Coordinates are Invalid
So let's write our last test case for DistanceCalc. As I said before, we want to make sure that if a negative number is given, an error is thrown:
<cfcomponent extends="net.sourceforge.cfunit.framework.TestCase">
<cffunction name="setUp" returntype="void" access="public">
<cfset VARIABLES.distcalc = CreateObject("component", "TDDWithCF.DistanceCalc").init() />
</cffunction>
<cffunction name="testValid" returntype="void" access="public">
<cfset var actual = VARIABLES.distcalc.getDistance(1, 4, 4, 0) />
<cfset var expect = 5 />
<cfset assertEquals("Distance calculation failed", expect, actual) />
</cffunction>
<cffunction name="testSameLocations" returntype="void" access="public">
<cfset var actual = VARIABLES.distcalc.getDistance(2, 3, 2, 3) />
<cfset var expect = 0 />
<cfset assertEquals("Distance calculation failed", expect, actual) />
</cffunction>
<cffunction name="testNegatives" returntype="void" access="public">
<cfset var errorThrown = false />
<cftry>
<!--- Call the getDistance and pass an invalid value. This should throw an error --->
<cfset VARIABLES.distcalc.getDistance(1, 2, 3, -4) />
<!--- Catch any errors --->
<cfcatch type="InvalidLocation">
<!--- Flag that an error was thrown --->
<cfset errorThrown = true />
</cfcatch>
</cftry>
<!--- Assert that an error was thrown --->
<cfset assertTrue("Expected error not thrown", errorThrown) />
</cffunction>
</cfcomponent>
This one looks a little more complicated, but it's really not that bad. All we do here is set up a local variable (errorThrown) to indicate that no error has been thrown yet. Then we call our method within <cftry> tags, passing in invalid data. This should throw an error, which we catch. If we do catch an error, we flag that an error was thrown. Finally, we assert that an error was actually thrown.
If you've put in the third test and you have refreshed your browser, you might be a bit puzzled. That's because the third test will still fail and will say, "Expected error not thrown". The reason is quite simple. If you look back at the code for DistanceCalc.cfc, you will see that there is no code to check if any of the arguments are negative values. To make our unit test pass, we need to update our getDistance method in the DistanceCalc CFC.
I'm not going to supply that code here; I'll leave that up to you. This is a small dose of TDD. As you try to resolve the issue you can keep refreshing the unit test. When the test passes, you have resolved the problem.
Unit Tests for index.cfm
Up to now, you have seen how one goes about testing a CFC. But what about the good old standard templates like our index.cfm file? At first glance, CFCs may seem to fit more naturally into a unit testing framework. To an extent that is true, but not as true as many believe.
Let me explain. I see a template (a *.cfm file) as a class - like a CFC - whose only function is to output HTML. Like a function in a CFC, the templates can accept arguments - via the FORM and URL scope. When working with templates, we are testing what the templates output when certain arguments are supplied.
Now I know what you are probably saying: "Not all templates only function to output HTML." That is quite true. Some templates have logic in them with other side effects. But it has become a widely accepted practice to place such application logic inside a CFC or UDF library, leaving only presentation logic in the templates. However, if you decide to place application or business logic in your templates, or if you are dealing with an old pre-CFC application, it is still easy to test those aspects. Simply <cfinclude> your template, and then assert the side effects the same way you would a CFC:
<cfset var expected = "hello world" />
<cfinclude template="x.cfm" />
<!--- VARIABLES.someVar is set inside x.cfm --->
<cfset assertEquals("someVar not set to the expected value", expected, VARIABLES.someVar) />
But what if your template is used for outputting HTML, like the index.cfm from our example? There is a special method called assertOutputs that is used to assert that a template (or a CFModule) outputs the expected text.
Let's build a unit test for our index.cfm file. As mentioned before, I start by trying to imagine all the different scenarios that might occur for the template:
- The template is initially loaded, outputting the form and header text.
- The form has been submitted and the distance is displayed along with the original form and header text.
So we will set up a new test case for our index file, with two test methods, like this:
TestIndex.cfc:
<cfcomponent extends="net.sourceforge.cfunit.framework.TestCase">
<cffunction name="testInitialState" returntype="void" access="public">
<cfset fail("Test not yet implemented") />
</cffunction>
<cffunction name="testResultsState" returntype="void" access="public">
<cfset fail("Test not yet implemented") />
</cffunction>
</cfcomponent>
Assuming you placed this CFC in the same location that you placed the TestDistanceCalc.CFC, you can execute this test by browsing to
http://localhost/TDDWithCF/tests/TestIndex.cfc?method=execute&verbose=1&html=1
Building a Test Runner: The Easier Way to Execute
If you are like me, you don't want to have to type these long URLs every time you execute a test case. Let's take a moment to create a test runner for ourselves, to make running these tests easier.
First, in your tests folder, create a new file named run.cfm, and paste in this code:
Run.cfm:
<!--- Set root to where the tests are located --->
<cfset VARIABLES.testroot = "TDDWithCF.tests" />
<!--- Create an array of our tests --->
<cfset VARIABLES.tests = ArrayNew(1) />
<cfset ArrayAppend(VARIABLES.tests, "#VARIABLES.testroot#.TestDistanceCalc") />
<cfset ArrayAppend(VARIABLES.tests, "#VARIABLES.testroot#.TestIndex") />
<!--- Create a test suite --->
<cfset VARIABLES.testsuite = CreateObject("component", "net.sourceforge.cfunit.framework.TestSuite").init( VARIABLES.tests ) />
<!--- Run all test in test suite --->
<cfset CreateObject("component", "net.sourceforge.cfunit.framework.TestRunner").run( VARIABLES.testsuite, "" ) />
Let's look at what this file does:
- First, it sets a variable for where our tests are located. If you placed your tests in a different folder then I have, you will need to change this value.
- It then creates an array, and fills it with our test cases. As your application grows and you add more test cases, you will only need to add them to the array to include them in this test runner.
- Next it creates a test suite, which is just a CFC used to group multiple test cases together.
- Finally, the tests in the test suite are executed using the TestRunner CFC.
Now simply browse to
http://localhost/TDDWithCF/tests/run.cfm (or wherever you saved the file). When you run the TestRunner, it will say that it ran five tests and that two failed. (If you have not resolved the problem for DistanceCalc, there will be three failures.) From now on you can execute all of your test cases for this application with this one URL.
The First Unit Test: The Template Loads Properly
Now that we have made our lives a little easier, let's get back to writing our unit test. Let's take a look at our first test method:
<cfcomponent extends="net.sourceforge.cfunit.framework.TestCase">
<cffunction name="testInitialState" returntype="void" access="public">
<cfset var expected = "" />
<cfset var template = "/TDDWithCF/index.cfm" />
<cfset expected = "<h1>Distance Calculator</h1>" />
<cfset assertOutputs(template, expected, "Header Missing") />
<cfset expected = "<form action=""index.cfm"" method=""POST"">" />
<cfset assertOutputs(template, expected, "Starting form tag missing") />
</cffunction>
<cffunction name="testResultsState" returntype="void" access="public">
<cfset fail("Test not yet implemented") />
</cffunction>
</cfcomponent>
The testInitialState method begins by setting two local variables, one for the expected outputs (initially empty) and a second for the template's location. Then we set the expected variable to the header HTML from the template, and assert that the template outputted the expected code. Finally, we check to make sure the template also outputted the opening form tag. This will insure that this template always outputs the page header and form tag. Refresh the test runner, and you should see one less failure.
Now let's look at our last test, testResultsState. Here we are testing that the distance was displayed in the form:
<cfcomponent extends="net.sourceforge.cfunit.framework.TestCase">
<cffunction name="testInitialState" returntype="void" access="public">
<cfset var expected = "" />
<cfset var template = "/TDDWithCF/index.cfm" />
<cfset expected = "<h1>Distance Calculator</h1>" />
<cfset assertOutputs(template, expected, "Header Missing") />
<cfset expected = "<form action=""index.cfm"" method=""POST"">" />
<cfset assertOutputs(template, expected, "Starting form tag missing") />
</cffunction>
<cffunction name="testResultsState" returntype="void" access="public">
<cfset var expected = "Distance: 0" />
<cfset var template = "/TDDWithCF/index.cfm" />
<cfset FORM.getresults = true />
<cfset assertOutputs(template, expected, "Distance results missing") />
</cffunction>
</cfcomponent>
This method is very similar to the testInitialState method. The main difference is that in testResultsState, I am first setting the FORM variable "getresult" to "true". If the template works as expected, this means the distance will be displayed on the page. I then assert that the text "Distance: 0" is in the output.
Does the testResultsState method give you a 100% guarantee that any unexpected behavior by this template will be caught? No. For example, someone might change the template to output "Dist.:" instead. At that time, they would need to update the unit test. But for such a small function I feel it is sufficient.
However, I do not feel the same way about the testInitialState method. If you look back at that test method, you will see that we are checking that the page has a header and an opening form tag. But is that all we want to test? There is so much more on the initial load that I feel should be validated, so we'll want to update that method to be stricter. Since we don't want to manually write all that out inside our test case, we can check our outputs against a text file that we will now create.
Creating a Text File
Let's now create a text file in the same folder as our test cases named indextest_initialstate.txt. We will then view the source of the index when we first visit it and copy/paste the HTML into the text file. (When you are doing this, make sure not to include any CF debug output.)
indextest_initialstate.txt
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>
<title>CFUnit DistCalc</title>
</head>
<body>
<h1>Distance Calculator</h1>
<form action="index.cfm" method="POST">
<div>
<h3>Location 1</h3>
X: <input name="x1" value="0" type="text" size="5"/> Y: <input name="y1" value="0" type="text" size="5"/>
</div>
<div>
<h3>Location 2</h3>
X: <input name="x2" value="0" type="text" size="5"/> Y: <input name="y2" value="0" type="text" size="5"/>
</div>
<input name="getresults" value="true" type="hidden"/>
<input type="submit"/>
</form>
</DIV>
</body>
</html>
Once we have done this, we will update the testInitialState method to this:
<cfcomponent extends="net.sourceforge.cfunit.framework.TestCase">
<cffunction name="testInitialState" returntype="void" access="public">
<cfset var expected = "indextest_initialstate.txt" />
<cfset var template = "/TDDWithCF/index.cfm" />
<cfset assertOutputs(template, expected, "Header Missing") />
</cffunction>
<cffunction name="testResultsState" returntype="void" access="public">
<cfset var e