Photo from Chile

Using MXUnit's injectMethod() to Reverse an injectMethod() call

I encountered a situation today in which I wanted to reverse the effects of an injectMethod() call in an MXUnit ColdFusion unit test. Here's some code that resembles my test case:

view plain print about
1<cfcomponent extends="mxunit.framework.TestCase">
2
3<cffunction name="setUp" access="public" returntype="void">
4    <cfscript>
5        variables.CUT = CreateObject("component","myComponentUnderTest");
6    
</cfscript>
7</cffunction>
8
9<cffunction name="test1IsActuallyProperlyNamed" access="public" returntype="void">
10    <cfscript>
11        injectMethod(variables.CUT, this, "overrideVerifyToTrue", "verify");
12        assertTrue(variables.CUT.aMethodThatWantsVerifyToReturnTrue());
13    
</cfscript>
14</cffunction>
15
16<cffunction name="test2IsActuallyProperlyNamed" access="public" returntype="void">
17    <cfscript>
18        injectMethod(variables.CUT, this, "overrideVerifyToTrue", "verify");
19        assertTrue(variables.CUT.anotherMethodThatWantsVerifyToReturnTrue());
20    
</cfscript>
21</cffunction>
22
23<cffunction name="test3IsActuallyProperlyNamed" access="public" returntype="void">
24    <cfscript>
25        injectMethod(variables.CUT, this, "overrideVerifyToTrue", "verify");
26        assertTrue(variables.CUT.aThirdMethodThatWantsVerifyToReturnTrue());
27    
</cfscript>
28</cffunction>
29
30<cffunction name="test4IsActuallyProperlyNamed" access="public" returntype="void">
31    <cfscript>
32        assertTrue(variables.CUT.aMethodThatWantsVerifyToBehaveAsCoded());
33    
</cfscript>
34</cffunction>
35
36<cffunction name="overrideVerifyToTrue" access="private" returntype="Any" hint="will be used as a test-time override with injectMethod()">
37    <cfreturn true />
38</cffunction>
39
40</cfcomponent>

From the above we can see that the first three tests want the method called verify() in the Component Under Test (CUT) to return TRUE, so we use injectMethod() to take a fake method (overrideVerifyToTrue), and use it to replace the actual verify() method in the CUT. The last test wants the verify() method in the CUT to behave as it's coded, so it doesn't call injectMethod(). This all works, but it introduces some duplication that I'd rather not have; I have to issue the same injectMethod() call in most of the tests. To remove that duplication I'd like to be able to move the call to injectMethod() into the setup() function. But, if I move injectMethod() into the setup() function, I'd need a way to "undo" the injectMethod() call in the final test.

I took a peek at the souce code for TestCase.cfc and ComponentBlender.cfc, and saw that it would not be possible to simply "undo" an injectMethod() call, as the original method would be long gone. I thought about a patch that would allow injectMethod() to actually save a copy of the method, which could then be restored by calling a new restoreMethod() function, but that seemed like a silly thing to add for an edge case like this.

I considered other routes, such as moving all of the tests that did not want verify() overridden into a separate test case, but then realized that I could simply use injectMethod() again to inject the correct method back into the CUT by instantiating a new copy of the CUT and injecting the method from it. Like so:

view plain print about
1<cfcomponent extends="mxunit.framework.TestCase">
2
3<cffunction name="setUp" access="public" returntype="void">
4    <cfscript>
5        variables.CUT = CreateObject("component","myComponentUnderTest");
6        injectMethod(variables.CUT, this, "overrideVerifyToTrue", "verify");
7    
</cfscript>
8</cffunction>
9
10<cffunction name="test1IsActuallyProperlyNamed" access="public" returntype="void">
11    <cfscript>
12        assertTrue(variables.CUT.aMethodThatWantsVerifyToReturnTrue());
13    
</cfscript>
14</cffunction>
15
16<cffunction name="test2IsActuallyProperlyNamed" access="public" returntype="void">
17    <cfscript>
18        assertTrue(variables.CUT.anotherMethodThatWantsVerifyToReturnTrue());
19    
</cfscript>
20</cffunction>
21
22<cffunction name="test3IsActuallyProperlyNamed" access="public" returntype="void">
23    <cfscript>
24        assertTrue(variables.CUT.aThirdMethodThatWantsVerifyToReturnTrue());
25    
</cfscript>
26</cffunction>
27
28<cffunction name="test4IsActuallyProperlyNamed" access="public" returntype="void">
29    <cfscript>
30        freshCUT = CreateObject("component","myComponentUnderTest");
31        injectMethod(variables.CUT, freshCUT, "verify", "verify");
32        assertTrue(variables.CUT.aMethodThatWantsVerifyToBehaveAsCoded());
33    
</cfscript>
34</cffunction>
35
36<cffunction name="overrideVerifyToTrue" access="private" returntype="Any" hint="will be used as a test-time override with injectMethod()">
37    <cfreturn true />
38</cffunction>
39
40</cfcomponent>

In the above example it may look like I removed a few lines of code only to have them replaced by additional lines of code elsewhere, but in the actual test case the number of tests is far greater, so the benefit is more obvious. I also extracted the code that creates the fresh CUT and injects a method from it into the CUT into a separate method, so I don't have to repeat those lines of code for each test that wants the original verify() method.

So, nothing earth shattering here, but I thought it was interesting to find a way of using injectMethod() that I hadn't considered before.

TweetBacks
Comments
Bob, why not just do another createObject call in the test function that needs the normal behavior, instead of using the CUT from setUp()?
# Posted By marc esher | 7/14/09 2:38 PM
The example isn't a direct copy of my scenario. There's actually quite a bit more setup() in my actual test case, including some mocking, all of which applies to all of the tests. I don't want to have to repeat all of that code in the final test when all I really want to do is "undo" the injectMethod().

I suppose I could extract all of that setup code into a separate method and then call that from both setup() and from the final test, but that still feels worse to me than what I've got.

I freely admit that this whole scenario is probably a result of sub-optimal test design, but I just thought it was cool that I could use injectMethod() to undo an injectMethod().
# Posted By Bob Silverberg | 7/14/09 3:18 PM
that's what i was figuring bob... that there was a lot more going on.

pretty neat solution you've got there.

i wonder: let's say you were on a team of developers who were "meh" about unit testing. would you still take this approach, or would you just copy/paste so that it'd be clearer what you were doing?

i've been thinking a lot about cleverness-vs.-concision in tests lately, which is why I ask.
# Posted By marc esher | 7/14/09 3:24 PM
I agree that there's definitely a trade-off between eliminating duplication and readability of tests. As a sole developer I only endeavour to create tests that will be understandable by me, so I'm probably not the best one to ask about this.

I would probably still write this test case in the same way, but perhaps add a comment before each injectMethod() explaining why it's there.
# Posted By Bob Silverberg | 7/14/09 4:06 PM
I find this not to be that big of an edge case. Our components are very complex and as such we use ColdSpring to manage our dependencies. Since the changes of injectMethod() end up applying to the ColdSpring singleton, I am forced to reload ColdSpring for each test that injects methods! This will be a HUGE timesaver and I believe should be included in the MXUnit source
# Posted By Dustin Chesterman | 10/6/10 2:07 PM
Clarification : I was referring to the restoreMethod() method. This particular scenario would not work for me since we use ColdSpring to grab the objects. But this could just drive me to do it! :)
# Posted By Dustin Chesterman | 10/6/10 2:10 PM
Gents,
restoreMethod() has been added to mxunit. You use it like so:

restoreMethod( object, "functionToRestore" );

Release notes are here: http://blog.mxunit.org/2010/11/mxunit-202-released...

Let me know how it works out when you get a chance to use it.

Marc
# Posted By Marc Esher | 11/18/10 9:02 PM
Awesome! Works great and test suites are running in a fraction of the time!
# Posted By Dustin Chesterman | 1/3/11 6:11 PM