Named vs Unnamed Arguments part II

I have just updated CFEasyMock to version 1.2.002 and the reflect package to 1.0.001 (cf7) and 2.1.002 (cf8) to fix a problem when using non-required arguments in function calls.

<cffunction name="aMethod" access="public" output="false">
<cfargument name="param1" required="false" />
<cfargument name="param2" required="false" />
<cfargument name="param3" required="false" />
</cffunction>

...

<cfset mockObj = mockFactory.createMock("MockObject") />
<cfset mockObj.aMethod(1, 2) />

Jack Ye noted that if you call this function with less than three parameters you will get an undefined error. The method invocation was not taking into consideration the required status of the argument.

With this fix, if you call the aMethod function with less than three parameters the mock will behave as expected. However, you mark an argument as required and do not pass it in, or do not pass in sufficient unnamed parameters to make sure the required positions are filled (not that I'd ever recommend coding that way!) then you will get an error, since required parameters should be passed.

For CF8 versions of CFEasyMock/reflect, the fix was applied to the onMissingMethod proxy mixin function.

For CF7 versions of CFEasyMock/reflect, the fix was applied to the ensureParameterNames method of the InvocationHandler. This method must be called in any extending component to correct the named vs. unnamed parameter function calls.

CFEasyMock v1.2 update

First, I've updated the CFEasyMock download zip to include CF7 again. Unfortunately where I work, the decision to upgrade to CF8 has been held off. Mostly by management, isn't it always?

So much of the work I've actually been able to devote to the package has occured on the CF7 version. Luckily, the primary difference between CF7 and CF8 versions of CFEasyMock lie outside of CFEasyMock itself, in the reflect package. The changes to CF7 were easy to integrate back up to the CF8 version.

Versions prior to 1.2 used a component called ExpectedMethodCall to hold the called method (as a reflect.Method reference) and the argument parameters for that call. Functions of ExpectedMethod call would do the matching to see if the expected and actual calls matched. Since this code is obviously the same for all method calls, it was moved into MockFactory, eliminating the need to create extra components just to perform matching calculations. The changes are completely internal and no unit test code needs to be altered.

1.1.001 corrects minor issues with Reflect 2.1

As I continue to tweak performance on both packages, I'm going to need to release minor updates.

I've modified the Reflect package onMissingMethod() mixin to perform the positional to named parameter conversion before invoking the InvocationHandler's invokeMethod function. This allowed me to removed the ensureParameterNames() from the InvocationHandler and refactor as an interface.

This refactoring of course required a few code tweaks in the MockInvocationHandler and sample code since CF is now enforcing the interface.

Also in Reflect, the Injectors are all placed in an array and processed with the same function. Less code this way, and it will allow for multiple injectors in the future. The primary injector used by Proxy is still MetaDataInjector, however it now extends FunctionDataInjector which implements the Injector interface. FunctionDataInjector is now located within the reflect package instead of sample.

Injector support for CFEasyMock

I've updated the reflect package to use an Injector component to do the work of populating the proxy method catalog. The proxy can be created with an external injector as well.

Sooo... this means that CFEasyMock now has been updated to take advantage of this hook. The new version 1.1 adds three new MockFactory methods to create mocks with an injector.

<cfset mf = createObject( "component", "easymock.MockFactory" ) />

<cfset injector = createObject( "sample.FunctionDataLoader" ).init( aFuncArrayYouCreate ) />

<cfset mock = mf.createInjectedMock( "sample.Collaborator", injector ) />

<cfset mock = mf.createInjectedStrictMock( "sample.Collaborator", injector ) />

<cfset mock = mf.createInjectedNiceMock( "sample.Collaborator", injector ) />

I've supplied the MetaDataInjector with the reflect package, which uses it internally as well. In the sample folder there is a FunctionDataInjector, which extends MetaDataInjector and can inject from just a function array (the data structure format from the metadata results of getMetaData() ).

Some of you have asked for injection, so now you can create your own injectors and use them in your mocks.

Performance Enhancements

The reflect package which CFEasyMock relies on to actually create the proxy has been updated to support the CF8 feature of onMissingMethod.

Nothing in the easymock package has changed. However, I will be updating the online zip and the SVN repository with the CF8 version of the Proxy component.

You will probably see a new project come up in a few days that will contain the reflect package itself. This is the package that does all the heavy lifting to create the proxy components and there are some cool ideas I'd like to implement, and these things aren't directly tied to the easymock package.

It will also allow me to version them seperately.

The primary benefit of moving to CF8 and onMissingMethod is speed. The CF7 version of reflect has to write a dummy component with the correct function call, create that file as a component instance, and then inject the function into the proxy. This can take considerable time if you have many mock objects.

Below are some times from unit tests for the ExpectedMethodCall, one of the biggest test cases in the package:

TEST CASECF7CF8
------------------------------------------------------
testEqualsMethodCallValid2140ms1156ms
testEqualsArgsFailStructCount1203ms625ms
testEqualsArgsFailComponent1218ms625ms
testEqualsArgsFailSimple1172ms641ms
testEqualsArgsFailQueryColumnlist1188ms609ms
testEqualsMethodCallInvalid1469ms890ms
testEqualsArgsFailArgCount1265ms625ms
testEqualsArgsFailQueryRecordCount1203ms610ms
testEqualsArgsFailStructKey1219ms625ms
testEqualsArgsValid1907ms609ms

Money where my mouth is...

You would think that given the context of the CFEasyMock package I would have a slurry of unit tests covering all aspects of the project from a very TDD perspective.

Alas, that is not the case. There were scattered tests that didn't cover much, other than basic creation of mocks and simple happy path tests.

Well, that has changed, or at least has mostly changed. I've gotten about 40% coverage now, and the unit tests use the CFEasyMock package.

I've found that the greatest benefit of CFEasyMock so far, has been the near elimination of having to do stepwise debugging to find out what a particular method is doing. You know the drill, code in output so you can see where things are happening. Run the method, dump values, see where things are occuring. Not so much anymore. I just use the strict mock and declare expected behavior.

I've added a tweak to the MockFactory and MockInvocationHandler that allows you to switch a mock into debug mode, collecting all method calls/return values as they go through, regardless of mode or expectation. The generated stack trace can be accessed by the stackTrace() function in the MockInvocationHandler itself.

<cfset mf = createObject( "component", "easymock.MockFactory" ) />
<cfset method = mf.createMock( "reflect.Method" ) />

<cfset test = createObject( "component", "test.Blank" ) />
<cfset other = createObject( "component", "sample.Collaborator" ) />

<cfset mf.expect( method.invokeMethod( test, structNew() ) ).andReturn( "Hello!" ) />
<cfset mf.expect( method.invokeMethod( other, structNew() ) ) />

<cfset mf.replay( method ) />
<cfset mf.debug( true, method ) />

<cfset method.invokeMethod( test, structNew() ) />
<cfset method.invokeMethod( other, structNew() ) />

<cfset mf.verify( method ) />

<cfdump var="#method.getProxyInvoker().stackTrace()#" />

I've also located and fixed the following issues:

  1. expects() was not triggering correctly due to the ResultMapper.add() not handling the first added request correctly.
  2. times() was overwriting the previous Result as well as the Range, instead of just modifying the previous Range.
  3. Component arguments were not compared at the name (aka type) level, which was allowing two of the same method call with different components to pass.
I've uploaded the latest build incorporating these changes v1.0.003

CFEasyMock available via SVN

Finally got CFEasyMock up on the SVN site. See the project home page on RIAForge for the URL.

Also, after feedback, before the 1.0 version goes final, I will probably be refactoring that package.

Also look for an explanation of the reflect package and how it is actually a separate package from easymock and has it's own uses!

New Behaviour classes added

Some of you who have experimented with CFEasyMock may have noticed that during record mode, the actual order of the function calls has no bearing on the way they are actually called and verified. The standard unordered behavior is used on any mock created by MockFactory.createMock().

But there are times when you need either a stricter level of control, or even less control. To accomodate this, CFEasyMock now has Strict and Nice mock behavior.

<cfset mf = createObject( "component", "easymock.MockFactory" ) />
<cfset strict = mf.createStrictMock( "test.component" ) />
<cfset nice = mf.createNiceMock( "test.component" ) />

For strict mocks, the order in which the functions are called during record mode, is matched against the order they are called in replay mode. So not only are you checking to make sure certain functions get called, but also called in a specific order.

Nice mocks are a twist on the standard unordered behavior. You can still add return values and expected function calls, but no errors are thrown by CFEasyMock if those expectations aren't fulfilled.

Let's say I have a Logger.cfc used by my component under test. In this instance, I don't really care if the logger is called during test, as it's primary purpose is just to be used for debugging output. However, I still need to mock it up and pass it into my component, since it's a required parameter.

<cfset mf = createObject( "component", "easymock.MockFactory" ) />
<cfset logMock = mf.createNiceMock( "Logger" ) />
<cfset mf.replay( logMock ) />

Now my test will run, and I won't have the noise of seeing everytime my logMock gets called unexpectedly, or have the overhead of having to declare all the expected behavior.

A note about nice mocks. Nice mocks attempt to return an empty value when there is an unexpected method call (0, "" or false, depending on returntype). If the component under test is expecting an object to be returned, be sure and declare that behavior using MockFactory.expect() or .expectLastCall().

You can also use the new behavior modifier anyTimes(), in addition to atLeastOnce().

<cfset mf = createObject( "component", "easymock.MockFactory" ) />
<cfset mock = mf.createStrictMock( "Logger" ) />

<cfset mf.expect( mock.foo( "Hello World!" ) ).anyTimes()

<cfset mf.replay( mock ) />

First Bug

While bugs are never a good thing, they are at least an idicator that someone out there is using your code. Ryan Wood found an issue with the reflect.Proxy component that would cause the server to infinitely recurse when the component to be mocked had a super component.

A 0.9c version has been uploaded with the fix.

Ryan also asked about SVN, and I'm working on getting that taken care of.

BlogCFC was created by Raymond Camden. This blog is running version 5.5.006.