Using MXUnit's injectMethod() to Reverse an injectMethod() call
Posted At : July 14, 2009 2:15 PM | Posted By : Bob Silverberg
Related Categories: MXUnit, ColdFusion
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:
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:
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.
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().
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.
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.
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