Using injectMethod for simple mocking - mxunit/mxunit GitHub Wiki

{toc}

Introduction

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>

Using injectMethod()

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.

Here’s the method signature for injectMethod():

<cffunction name="injectMethod" output="false" access="public" returntype="void" hint="injects the method from giver into receiver. This is helpful for quick and dirty mocking">
	<cfargument name="Receiver" type="any" required="true" hint="the object receiving the method"/>
	<cfargument name="Giver" type="any" required="true" hint="the object giving the method"/>
	<cfargument name="FunctionName" type="string" required="true" hint="the function to be injected from the giver into the receiver"/>
	<cfargument name="FunctionNameInReceiver" type="string" required="false" default="#arguments.functionName#" hint="the function name that you will call. this is useful when you want to inject giver.someFunctionXXX but have it be called as someFunction in your receiver object">

</cffunction>

{*}Almost always:*
  • “Receiver” is your component under test
  • “Giver” is your test itself since that’s where the overriding function will be declared, so you’ll use “this”
  • “functionName” will the name of the overriding function you’ve defined in your test that will be used to overwrite the function in the component under test
  • “functionNameInReceiver” is the function you’re overriding

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.

Difference from Mocking

I can’t stress enough that this solves a different problem than mock objects solve. Mocks solve the problem of replacing full-featured collaborator objects with controlled-behavior stand-ins at time of test. 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.

Using restoreMethod()

{warning}We strongly encourage you to write tests that use freshly-created instances in your setUp() method.{warning}
If you have tests that reuse the same component instance, injectMethod() can wreak havoc. Usually you wind up in this situation if you’re using coldspring-managed objects inside your TestCase. To undo a method overwrite resulting from injectMethod(), you can use restoreMethod( object, “functionName” ). This will restore the original function back into the component.

MXUnit will NOT automatically restore methods for you. You must do so deliberately. I advise doing it in tearDown, like so:

<cffunction name="tearDown">
   <cfset restoreMethod( someObject, "someFunction" )>
</cffunction>
⚠️ **GitHub.com Fallback** ⚠️