How to test private methods

In ColdFusion a CFC's methods can be marked as private (or package), which means no other CFC can access that method (or that only CFCs in the same folder can access it). This can be a problem in CFUnit when a private method needs to be used in a unit test.

If you encounter this problem, we first suggest you evaluate whether or not you truly should be unit testing that method in the first place. If it is marked private, that is because it is a internal behavior that should only be invoked by that CFC. Most likely there are other, public, methods that will be invoking this method to perform their work. You might be better off testing all those method, and therefore verifying this private method too.

However, it is our experience that there are times that you truly do need to test the private method directly. You could change this methods access to public, just for the sake of CFUnit. That would be the easiest solution. However, the point of CFUnit is to improve the stability of your code, not reduce it by preventing you from building the CFC as it should be built.

So we will cover one alternative solution that will allow us to test those private methods, while still maintaining the integrity of the CFC.

Lets start by setting up a scenario.

Place the following three files in a "CFUnitHelp" folder off the root of your server.

MyCFC.cfc

<cfcomponent displayname="MyCFC">
 	<cfproperty name="temp1" type="string">
	<cfproperty name="temp2" type="string">
	
	<cffunction name="init" returntype="MyCFC">
		<cfset VARIABLES.temp1 = 3>
		<cfset VARIABLES.temp2 = 2>
		<cfreturn THIS>
	</cffunction>
	
	<cffunction name="add" access="public" returntype="numeric">
			<cfreturn (VARIABLES.temp1 + VARIABLES.temp2)>
	</cffunction>
	
	<cffunction name="sub" access="private" returntype="numeric">
			<cfreturn (VARIABLES.temp1 - VARIABLES.temp2)>
	</cffunction>
</cfcomponent>

MyCFCTest.cfc

<cfcomponent displayname="MyCFCTest" extends="net.sourceforge.cfunit.framework.TestCase">
	<cfproperty name="tempCFC" type="MyCFC">
	
	<cffunction name="setUp" returntype="void" access="public">
		<cfset VARIABLES.tempCFC = CreateObject("component", "CFUnitHelp.MyCFC").init()>
	</cffunction>
	
	<cffunction name="testAdd" returntype="void" access="public">
		<cfset result = VARIABLES.tempCFC.add()>
		<cfinvoke method="assertEquals">
			<cfinvokeargument name="expected" value="#numberFormat(5.0)#">
			<cfinvokeargument name="actual" value="#numberFormat(result)#">
		</cfinvoke>
	</cffunction>
	
	<cffunction name="testSub" returntype="void" access="public">
		<cfset result = VARIABLES.tempCFC.sub()>
		<cfinvoke method="assertEquals">
			<cfinvokeargument name="expected" value="#numberFormat(1.0)#">
			<cfinvokeargument name="actual" value="#numberFormat(result)#">
		</cfinvoke>
	</cffunction>

</cfcomponent>

mytest.cfm

<cfsilent>
	<cfset testClasses = ArrayNew(1)>
	<cfset ArrayAppend(testClasses, "CFUnitHelp.MyCFCTest")>
	<!--- Add as many test classes as you would like to the array --->
	<cfset testsuite = CreateObject("component", "net.sourceforge.cfunit.framework.TestSuite").init( testClasses )>
</cfsilent>

<cfoutput>
	<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">

	<html>
	<head>
		<title>Unit Test Example</title>
	</head>

	<body
	<h1>CFUnit Test</h1>
	<cfinvoke component="net.sourceforge.cfunit.framework.TestRunner" method="run">
		<cfinvokeargument name="test" value="#testsuite#">
		<cfinvokeargument name="name" value="">
	</cfinvoke>
	</body>
	</html>
</cfoutput>
No browse to http://{your server domain}/CFUnitHelp/mytest.cfm You will see that two tests ran, but one threw the following error: testSub(CFUnitHelp.MyCFCTest): The method 'sub' could not be found in component.

The test was not able to find the 'sub' method because it is marked as private, and therefore invisible to other CFCs or CFML templates. So we have successfully replicated our problem scenario.

To solve this we will create a 'publisher' CFC. Create a new CFC called MyCFCPublisher

MyCFCPublisher.cfc

<cfcomponent displayname="MyCFCPublisher" extends="CFUnitHelp.MyCFC">
	<cffunction name="sub" access="public" returntype="numeric">
		<cfreturn SUPER.sub()>
	</cffunction>
</cfcomponent>
What this CFC does is extends the CFC we wish to test. Therefore it will inherit all its properties and methods. We then override the private method(s), with a public one. But all we do in this method is call the SUPER class's method. We can do that here because in CFML private methods are available to any subclass of the CFC as well as the original class. We can "publish" any private CFC like this as long as you give the new method the same 'footprint'. The means the new method must have: If the original CFC has any method, we simple need to pass those when the call the super class's method:
    <cfreturn SUPER.someMethod( ARGUMENTS.arg1, ARGUMENTS.arg2 )>
Of course if you refresh the browser you will still get the same error. That is because we have to tell our tester CFC that it should use the new publisher CFC to run its tests. To do this simple replace any reference of "MyCFC" in the "MyCFCTest" with "MyCFCPublisher"". Here is what our new CFCTester code looks like:

MyCFCTester.cfc

<cfcomponent displayname="MyCFCTest" extends="net.sourceforge.cfunit.framework.TestCase">
	<cfproperty name="tempCFC" type="MyCFC">
	
	<cffunction name="setUp" returntype="void" access="public">
		<cfset VARIABLES.tempCFC = CreateObject("component", "CFUnitHelp.MyCFCPublisher").init()>
	</cffunction>
	
	<cffunction name="testAdd" returntype="void" access="public">
		<cfset result = VARIABLES.tempCFC.add()>
		<cfinvoke method="assertEquals">
			<cfinvokeargument name="expected" value="#numberFormat(5.0)#">
			<cfinvokeargument name="actual" value="#numberFormat(result)#">
		</cfinvoke>
	</cffunction>
	
	<cffunction name="testSub" returntype="void" access="public">
		<cfset result = VARIABLES.tempCFC.sub()>
		<cfinvoke method="assertEquals">
			<cfinvokeargument name="expected" value="#numberFormat(1.0)#">
			<cfinvokeargument name="actual" value="#numberFormat(result)#">
		</cfinvoke>
	</cffunction>

</cfcomponent>
Now if you refresh the browser, you will see that both test now pass.

However, please note, that this method does have its drawbacks. Primarily it can increase the maintenance of your CFCs. Changes to the original CFC that changes the footprint of any published method will need to be reflected in the published CFC. Also, of there is an error, it can be a bit confusing if for some reason that error is caused by the publisher itself. Therefore, this tactic should be used only when necessary, when there is no alternative way to test the behavior.