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:


<cfcomponent extends="mxunit.framework.TestCase">

<cffunction name="setUp" access="public" returntype="void">
    <cfscript>
        variables.CUT = CreateObject("component","myComponentUnderTest");
    
</cfscript>
</cffunction>

<cffunction name="test1IsActuallyProperlyNamed" access="public" returntype="void">
    <cfscript>
        injectMethod(variables.CUT, this, "overrideVerifyToTrue", "verify");
        assertTrue(variables.CUT.aMethodThatWantsVerifyToReturnTrue());
    
</cfscript>
</cffunction>

<cffunction name="test2IsActuallyProperlyNamed" access="public" returntype="void">
    <cfscript>
        injectMethod(variables.CUT, this, "overrideVerifyToTrue", "verify");
        assertTrue(variables.CUT.anotherMethodThatWantsVerifyToReturnTrue());
    
</cfscript>
</cffunction>

<cffunction name="test3IsActuallyProperlyNamed" access="public" returntype="void">
    <cfscript>
        injectMethod(variables.CUT, this, "overrideVerifyToTrue", "verify");
        assertTrue(variables.CUT.aThirdMethodThatWantsVerifyToReturnTrue());
    
</cfscript>
</cffunction>

<cffunction name="test4IsActuallyProperlyNamed" access="public" returntype="void">
    <cfscript>
        assertTrue(variables.CUT.aMethodThatWantsVerifyToBehaveAsCoded());
    
</cfscript>
</cffunction>

<cffunction name="overrideVerifyToTrue" access="private" returntype="Any" hint="will be used as a test-time override with injectMethod()">
    <cfreturn true />
</cffunction>

</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:


<cfcomponent extends="mxunit.framework.TestCase">

<cffunction name="setUp" access="public" returntype="void">
    <cfscript>
        variables.CUT = CreateObject("component","myComponentUnderTest");
        injectMethod(variables.CUT, this, "overrideVerifyToTrue", "verify");
    
</cfscript>
</cffunction>

<cffunction name="test1IsActuallyProperlyNamed" access="public" returntype="void">
    <cfscript>
        assertTrue(variables.CUT.aMethodThatWantsVerifyToReturnTrue());
    
</cfscript>
</cffunction>

<cffunction name="test2IsActuallyProperlyNamed" access="public" returntype="void">
    <cfscript>
        assertTrue(variables.CUT.anotherMethodThatWantsVerifyToReturnTrue());
    
</cfscript>
</cffunction>

<cffunction name="test3IsActuallyProperlyNamed" access="public" returntype="void">
    <cfscript>
        assertTrue(variables.CUT.aThirdMethodThatWantsVerifyToReturnTrue());
    
</cfscript>
</cffunction>

<cffunction name="test4IsActuallyProperlyNamed" access="public" returntype="void">
    <cfscript>
        freshCUT = CreateObject("component","myComponentUnderTest");
        injectMethod(variables.CUT, freshCUT, "verify", "verify");
        assertTrue(variables.CUT.aMethodThatWantsVerifyToBehaveAsCoded());
    
</cfscript>
</cffunction>

<cffunction name="overrideVerifyToTrue" access="private" returntype="Any" hint="will be used as a test-time override with injectMethod()">
    <cfreturn true />
</cffunction>

</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.

Comments
marc esher's Gravatar 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
Bob Silverberg's Gravatar 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
marc esher's Gravatar 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
Bob Silverberg's Gravatar 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