Thursday 4 July 2013

Repro case for "contains" pseudo-reserved-word interfering with Mockbox

G'day:
This is mostly for Brad Wood, but it might be of passable interest to others, so I'll plonk it here.

Yesterday's article discussed how contains is kind of a reserved word, but kind of not in ColdFusion (it's just not in Railo). I observed to Brad that this actually bites us on the bum with Mockbox, and he asked for more info, so here it is.

Basically we use a caching system which has a method "contains" which checks to see if there's an item with a given key already in the cache, before trying to fetch it. We've actually since revised this approach, but we have some legacy code still using methods called "contains". So we need to unit test them, and indeed their responses play a part in other methods we test. When testing these other methods which use the caching system, we mock-out the cache, and the methods within it, and we use Mockbox to do this. Mockbox is cool, btw. You should be using it if yer not already.

We're still using Mockbox 1.3 (more about why further down), and it is impossible to directly mock a method called contains using Mockbox. We've worked around this, but it took a bloody long time to work out what the hell was going on, and that there was working-around to do.

Here's a portable (-ish) repro case.

// Application.cfc
component {

    this.name        = "testMockbox";
    this.mappings    = {
        "/mockboxissue"    = expandPath("."),
        "/mockbox"        = expandPath("./mockboxes/1_3") // this is where we have the mockbox app located
    };

}

Application.cfc just sets up some mappings to make the code as portable as I can. The only requirement is that the directory these files go in must have the mockboxes/1_3 holding mockbox in it.

// ToTest.cfc
component {

    public ToTest function init(required ToMock toMock){
        variables.toMock = arguments.toMock;
        return this;
    }

    public void function callsContains() {
        variables.toMock.contains();
    }

    public void function callsOther() {
        variables.toMock.other();
    }

}

ToTest.cfc is the CFC we are wanting to test. Note that it contains another object - toMock - which has its own methods. As we're only wanting to test ToTest.cfc, we mock-out the ToMock instance.

<!--- ToMock.cfc --->
<cfcomponent>

    <cffunction name="contains" returntype="void" access="public">
    </cffunction>

    <cffunction name="other" returntype="void" access="public">
    </cffunction>


</cfcomponent>

Obviously this is just a stub, but it will demonstrate the point.

Here's the actual test code. Normally this would be within some MXUnit unit tests, but setting it up that way makes it less portable, so I'm just putting the "tests" in a CFM file.

Our tests are to make sure that when we call callsOther() and callsContains(), that they respectively call other() and contains(). That's all we need to test here. We don't want other() or contains() to actually run. In the real world they have dependencies on external systems, so cannot be used in a unit test; nor do we really want to when that's not what we're testing.

Anyway... the code...

<cfscript>
// test.cfm

oMockbox = createObject("mockbox.system.testing.MockBox").init();


// test mocking other() method, as a control: this works fine
toMock = oMockbox.createMock("mockboxissue.ToMock");
toMock.$("other");
callLog = toMock.$callLog();
toTest = createObject("mockboxissue.ToTest").init(toMock=toMock);

toTest.callsOther();
writeDump(var=callLog, label="callLog after call to callsOther()");    // callLog() shows other() was called once. Correct

I'll just interrupt at this point. Here we are doing this:
  • mocking toMock
  • and the other() method
  • then initialising our test object with the mocked toMock object
  • then we call the method we're testing, callsOther()
  • and verify in the call log that other() has been called.
And here's the output:

callLog after call to callsOther() - struct
other
callLog after call to callsOther() - array
1
callLog after call to callsOther() - struct [empty]

So, yes, other() was called. Perfect.

Now the code continues, and we do exactly the same thing, except with the callsContains() / contains() methods:


// test mocking contains() method directly. Same code as above, just mocking / calling contains() instead of other()
toMock = oMockbox.createMock("mockboxissue.ToMock");
toMock.$("contains");
callLog = toMock.$callLog();
toTest = createObject("mockboxissue.ToTest").init(toMock=toMock);

try {
    toTest.callsContains();
    writeDump(var=callLog, label="callLog after call to callsContains()");
} catch (any e){
    writeDump([e.type,e.message,e.detail, e.tagcontext[1].raw_trace]);
}

I've had to put a try/catch in here, as this code errors:

array
1Application
2The method contains was not found in component D:\websites\www.scribble.local\hb\mockboxissue\ToMock.cfc.
3Ensure that the method is defined, and that it is spelled correctly.
4at cfToTest2ecfc174535578$funcCALLSCONTAINS.runFunction(D:\websites\www.scribble.local\hb\mockboxissue\ToTest.cfc:10)

So Mockbox has not only not mocked contains(), it's actually removed the original method too! Suck. Obviously it's really not clear what's happening here, so this took a while to sort out. We didn't ever think to check whether Mockbox was at fault here.

Ultimately we were able to work around this adequately:


// test mocking contains() method via proxy
toMock = oMockbox.createMock("mockboxissue.ToMock");

// mock a proxy...
toMock.$("containsProxy");
// ... then use the proxy in place of contains()
toMock.contains = toMock.containsProxy;
callLog = toMock.$callLog();
toTest = createObject("mockboxissue.ToTest").init(toMock=toMock);

toTest.callsContains();
writeDump(var=callLog, label="callLog after call to callsContains()");    // this reports containsProxy() was called, by which one can infer contains() was actually called
</cfscript>

The call log this time says this:

callLog after call to callsContains() - struct
containsProxy
callLog after call to callsContains() - array
1
callLog after call to callsContains() - struct [empty]

This is OK, because the method is actually containsProxy(), we are just creating a reference to it, contains(). However that containsProxy() has a call count, this means contains() was called. Sorted.

But what's the problem here?

Well I decided to upgrade Mockbox from the version we were using to the current version to see if this issue was resolved. And, I recall now... we were having another issue... can't remember what that was now! So if I try to run the contains() test again on Mockbox 2.0, I now get this:

Invalid CFML construct found on line 2 at column 51.

ColdFusion was looking at the following text:
contains

The CFML compiler was processing:

A cfset tag beginning on line 2, column 26.

The error occurred in D:/websites/www.scribble.local/hb/mockboxissue/mockboxes/2_0/system/testing/stubs/A17E6A63-D067-E5E6-F12E7FF7DCFFF7C6.cfm: line 2

In one way, this seems like it's even worse because we're now getting a compile error, not just a runtime error (we can work around the latter). And it certainly means we cannot upgrade to 2.0. However on the other hand, I now have a hint where Mockbox is going wrong, as I can look at A17E6A63-D067-E5E6-F12E7FF7DCFFF7C6.cfm. In A17E6A63-D067-E5E6-F12E7FF7DCFFF7C6.cfm, we have this:


<cfset this["contains"] = contains>
<cfset variables["contains"] = contains>
<cffunction name="#arguments.method#" access="#fncMD.access#" output="#fncMD.output#" returntype="#fncMD.returntype#">
// [more stuff snipped as it's not relevant]

So this is the code Mockbox writes out to mock the method. And unfortunately, those two <cfset> statements are not valid syntax in ColdFusion's CFML, as this is another instance in which contains actually is a reserved word, so cannot be used like that.

TBH, I think that code is not quite right anyhow. It seems to me creating a function, then making sure the function is available in the variables and this scope (reading from bottom to top, as the function declaration statements are executed first). However the function will already be put in the variables scope as part of it being created, so this line is redundant:

<cfset variables["contains"] = contains>

And also that the previous line can simply be this:
<cfset this["contains"] = variables["contains"]>

That is not a syntax error any more, so it will compile fine.  I have tweeaked my copy of Mockbox, and doing this seems to fix the issue (I have not verified it doesn't actually cause other problem... I'll leave it to Brad to sort that out ;-)


Anyway, there's a repro case for Brad, a fix for Mockbox 2.0 for me (hurrah!), and yet another story of me fighting with bugs.

Incidentally, and as I said above: none of this is a problem on Railo. contains is not a reserved word of any description in Railo, as far as I can tell.

Righto.

--
Adam