TransferMockFactory

I've added a new package to the easymock library called "ext" to allow for a external system hooks to be bundled with easymock.

The first of these is the Transfer extension which adds the TransferMockFactory. TransferMockFactory is capable of building mock objects containing all the functions that the TransferObject contains, along with any Decorator functions. Best part is, it used the transfer configuration file in your application to do so. No more hard-wiring or manually injecting to build out a mock object when using Transfer.

First off, let me say that this is still beta code, as far as the easymock.ext.transfer package is concerned. The easymock package has been slightly modified to work with reflect.Injector objects when called from the createInjectedXXXMock() functions.

I am assuming that very few people, and by very few I mean zero, were ever using the createInjectedXXXMock() functions anyway.

A few notes on the TranferMockFactory. - ... is not an extension of easymock.MockFactory and TransferMockFactory is NOT a static class. It does have state. The init() method requires two parameters: the path to the tranfer.xml configuration file (relative to the root) and the absolute path to the root. You can have more than one TransferMockFactory depending on your application configuration. - ...uses Transfer objects to create the metadata for the TransferObject, then converts that to the component metadata for FUNCTIONS. - ...makes use of reflect v2.5 (included with this version of CFEasyMock) which introduced a newInjectedProxyInstance() to clean up the code. - ... returns a mock with a type of either transfer.com.TransferObject or the decorator component listed in the transfer.xml configuration for that object. - ... doesn't currently implement the ParentOneToMany concept. Will hopefully post an update on this soon.

It would be nice to have options for creating the TransferMockFactory, such as passing in the Configuration component from Transfer to keep from having to duplicate file location code. Hopefully in a future release. I would greatly appreciate feedback on this.

For more detail see the documentation included in the v1.5 download of CFEasyMock.

easymock.samples

Fixed the samples directory code. Now, the tests show a successful test, a test with a failure on verify, an unexpected method call and also pass/fail tests to demonstrate internal behavior of method under test when collaborating with a mock.

Arguments and Return Values and NULLs ... oh MY!

I had thought the argument monster had finally died with my last post. But noooo. There's yet another wrinkle, and this time it is actually not my fault.

Consider the following code.

<cffunction name="foo">
<cfargument name="param1" required="false" />
<cfargument name="param2" required="false" />

<cfdump var="#structKeyExists( arguments, "param1" )#" />
<cfdump var="#structKeyList( arguments )#" />
<cfdump var="#arguments#" />

<cfset bar( argumentCollection=arguments ) />
</cffunction>

<cffunction name="bar">
<!--- do nothing --->
</cffunction>

<!--- Run the function --->
<cfset foo() />

Okay, so guess what you see? First, you'll notice the obvious "NO" coming from the structKeyExists() function. We expect that, and love it for behaving as expected.

And now it just starts getting a little weird. You'll notice the list of arguments has both "param1" and "param2" in the list. What? I didn't pass in any values why would they be in the list, especially if they don't 'exist'? Well, take a look at the next output.

The dump of the arguments struct provides the answer. Notice that pesky little phrase 'undefined struct element'? CF has predefined the argument collection keys, and then places the corresponding values. Since there was no value passed, the java side of CF places NULLs into their slots. The structKeyExists() function correctly determines existence if the value is NULL, but that's in java. CF doesn't have a way to directly detect for nulls.

Be very careful when dealing with non-required parameters and then passing the arguments struct off as an argumentCollection.

Just for kicks try this:

<cffunction name="nullify">
<!--- do nothing --->
</cffunction>

<cfset foo = "Hello World!" />
<cfset foo = nullify() />
<cfoutput>
#foo#
</cfoutput>

It's easy to spot the problem here, but in real world code where the offending function is more than likely in another file, it can cause a bit of a headache.

Be default the java methods that the CF code gets translated into will return a null value if a return value is not specified. The nullify function above essentially is the same as:

<cffunction name="nullify">
<cfreturn javaCast( "null", "" ) />
</cffunction>

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

More Entries

BlogCFC was created by Raymond Camden. This blog is running version 5.5.006. | Protected by Akismet | Blog with WordPress