Download to get rid of those pesky bugs.             MXUnit Unit Testing framework for ColdFusion developers                               Download       Details       Home       Support       Blog

Using injectMethod for simple mocking

A while back, I (Marc) wrote about using coldfusion's "mix-in" functionality to achieve simple mocking. The usefulness here is where you have a function that "does something", but you want to change the thing that it does for the purpose of a test. This is particularly handy when you're testing functionA(), and functionA() calls functionB() and functionC(). Maybe functionA() depends on the results of functionB() to do certain work, and then it calls functionC() to do other work. In code, it might look like:

        <cfcomponent name=MyComponent>
                <cffunction name="functionA">
                        <cfargument name="someArg" required="true">
                        <cfif listlen(functionB(someArg) GTE 1)>
                                <cfreturn functionC(someArg)>                     
                        </cfif>
                        <cfreturn false>
                </cffunction>
                
                <cffunction name="functionB">
                        .... maybe i'll return a number ... or a list of numbers
                </cffunction>
                
                <cffunction name="functionC">
                        .... i think i'll go and run a bunch of database updates
                        <cfreturn true>
                </cffunction>
        </cfcomponent>
        

And here might be some tests for functionA:

        <cfcomponent extends="mxunit.framework.TestCase">
                
                <cffunction name="setUp">
                        <cfset obj = createObject("component","MyComponent")>
                </cffunction>
                
                <cffunction name="functionAShouldReturnFalseForASingleListElement">
                        <cfset ret = obj.functionA(SomeID)>
                        <cfset assertFalse(ret,"a single list should've been returned for SomeID and functionA should have returned false")>
                </cffunction>
                
                <cffunction name="functionAShouldReturnFalseForMultipleListElements">
                        <cfset ret = obj.functionA(SomeOtherID)>
                        <cfset assertFalse(ret,"multiple list elements should've been returned for SomeID and functionA should have returned false")>
                </cffunction>
                
                <cffunction name="functionAShouldReturnTrueForNoElements">
                        <cfset ret = obj.functionA(AndYetAnotherID)>
                        <cfset assertTrue(ret,"NO list elements should've been returned for SomeID and functionA should have returned true")>
                </cffunction>
        </cfcomponent>
        

Now, let's say functionB() queries the database or whatever, based on the passed-in someArg argument. The problem is obvious: your database is in an unknown state, because data change all day long. And you want to do a number of tests: you want to test the condition where functionB() returns a single list element, and also when it returns more than 1 list element. Which means you need at least two known inputs for someArg: one that will ensure functionB() returns a single element, and one that ensures it'll return more than one. What a pain! Wouldn't it be great if you could say "for the purposes of this test, I want functionB() to return a single list element". and then in another test, say "And for this test, I want it to return 2 list elements"? Or, to put it another way, wouldn't it be nice to override functionB for this test, but without a lot of work?

This is why injectMethod() was born. To make it a little easier to override functions for the purpose of testing. Now, you're not overriding the function under test! You're overriding functions that the function under test calls, in order to make it easier to test the function under test. Let's have a look at our new set of tests:

        <cfcomponent extends="mxunit.framework.TestCase">
                
                <cffunction name="setUp">
                        <cfset obj = createObject("component","MyComponent")>
                </cffunction>
                
                <!---  DEFINE PRIVATE METHODS TO OVERRIDE FUNCTIONB AND FUNCTIONC  --->
                
                <cffunction name="returnsSingleListElement" access="private">
                        <cfreturn "1">
                </cffunction>
                
                <cffunction name="returnsMultipleListElements" access="private">
                        <cfreturn "1,2,3">
                </cffunction>
                
                <cffunction name="returnsNoListElement" access="private">
                        <cfreturn "">
                </cffunction>
                
                <!---  and our tests, again  --->
                <cffunction name="functionAShouldReturnFalseForASingleListElement">
                        <!--- pass in our returnSingleListElement function into the object and name it functionB (i.e., override functionB) inside the object under test --->
                        <cfset injectMethod(obj, this, "returnSingleListElement", "functionB")>
                        <cfset ret = obj.functionA(SomeID)>
                        <cfset assertFalse(ret,"a single list should've been returned for SomeID and functionA should have returned false")>
                </cffunction>
                
                <cffunction name="functionAShouldReturnFalseForMultipleListElements">
                        <!--- pass in our returnMultipleListElements function into the object and name it functionB --->
                        <cfset injectMethod(obj, this, "returnMultipleListElements", "functionB")>
                        <cfset ret = obj.functionA(SomeOtherID)>
                        <cfset assertFalse(ret,"multiple list elements should've been returned for SomeID and functionA should have returned false")>
                </cffunction>
                
                <cffunction name="functionAShouldReturnTrueForNoElements">
                        <!--- pass in our returnNoListElement function into the object and name it functionB --->
                        <cfset injectMethod(obj, this, "returnNoListElement", "functionB")>
                        <cfset ret = obj.functionA(AndYetAnotherID)>
                        <cfset assertTrue(ret,"NO list elements should've been returned for SomeID and functionA should have returned true")>
                </cffunction>
        </cfcomponent>
        

As this illustrates, we've now created a very easy way to test functionA with the 3 cases we need to happen with functionB: a single list, multiple list, and no-element returns. Now, to take this one step further, you could override functionC -- which, if you remember, updates the database -- with a simple function that simply returns "true". Remember, we're not testing functionC so ideally we wouldn't touch the database at all in this case

        <cfcomponent extends="mxunit.framework.TestCase">
                
                <cffunction name="setUp">
                        <cfset obj = createObject("component","MyComponent")>
                </cffunction>
                
                <!---  DEFINE PRIVATE METHODS TO OVERRIDE FUNCTIONB AND FUNCTIONC  --->
                
                ....
                
                <cffunction name="functionC_Replacement" access="private">
                        <cfreturn true>
                </cffunction>
                
                <!---  and our tests, again  --->
                <cffunction name="functionAShouldReturnTrueForNoElements">
                        <!--- pass in our returnNoListElement function into the object and name it functionB
                                        in addition, overwrite functionC with our new, spoof functionC   --->
                        <cfset injectMethod(obj, this, "returnNoListElement", "functionB")>
                        <cfset injectMethod(obj, this, "functionC_Replacement", "functionC")>
                        <cfset ret = obj.functionA(SomeID)>
                        <cfset assertTrue(ret,"NO list elements should've been returned for SomeID and functionA should have returned true")>
                </cffunction>             
                
                ....
                
        </cfcomponent>
        

There you go: you can pass in functions to achieve exactly the conditions you want to achieve in order to fully test your logic. And you pass in functions that "spoof" the DB-updating function that would slow down your test and potentially corrupt your data.

I can't stress enough that this solves a different than mock objects solve. Mocks solve the problem of spoofing collaborator objects. But in this case, we're not spoofing functions in a dependent component. We're spoofing functions in the same component we're trying to test.


2008 MXUnit.org