Matchers

Prior to version 2.0, expected and actual behaviour was determined by performing equality checks between the method signatures of the expected calls and the actual calls. All function parameters were checked for direct equality. (Objects/components were the exception and were just checked for type.)

Setting expected behaviours would work fine as long as the method under test doesn't pass internally generated data, such as a date or a UUID, into the mock.

As you can see, this behaviour can be limiting. Take the example of a CacheManager object that sets a creation date on a mocked CacheableItem.

<cfinterface name="CacheableItem">
<cffunction name="setCacheDate" />
</cfinterface>

<cfcomponent name="myItem">
<cffunction name="setCacheDate">
...
</cffunction>
</cfcomponent>

<cfcomponent name="CacheManager">
<cffunction name="add">
<cfargument name="cacheItem" required="yes" type="CacheableItem">

   <cfset arguments.cachedItem.setCacheDate( now() ) />

   <!--- add item to cache, etc. --->
</cffunction>
</cfcomponent>

Now we attempt to write a unit test for the CacheManager.

<cfcomponent name="testCacheManager" extends="mxunit.framework.TestCase">
<cffunction name="testAdd">
<cfset mf = createObject( "component", "easymock.MockFactory" ) />
<cfset ciMock = mf.createMock( "myItem" ) />
<cfset mgr = createObject( "component", "CacheManager" ) />

<cfset mf.expect( ciMock.setCacheDate( now() ) ) />

<cfset mf.replay( ciMock ) />

<cfset mgr.add( ciMock ) />

<cfset mf.verify( ciMock ) />
</cffunction>
</cfcomponent>

Everything looks like it's in order. We're calling the setCacheDate() function and passing it the same value as in the CacheManager. However, when this test runs it will fail (unless your computer is infinitely fast). Why? Because the timestamp value returned from now() is not the same as the value passed when the test calls the add() function. So how do you tell the mock to expect something that "matches" a given input, but doesn't have to "equal" that input?

This is where matchers come in to play. Version 2.0 introduces matchers as a way to manage expected parameters when a specific data match is not necessary or impossible to reproduce.

In the above example, we would change the expect() call on the ciMock to use one of the matchers provided by CFEeasyMock

...
<cfset mf.expect( ciMock.setCacheDate( mf.after( '1900-01-01' ) ) ) />
...

The mf.after() function call returns a DateMatcher that will return true for any value on or after January 1, 1900. If no matcher is specified, the value passed to the mock function call during record phase is wrapped in a matcher that matches based on equal values. So the following two expectations are identical to CFEasyMock in record mode:

<cfset mf.expect( mock.setName( "Rumplestiltskin" ) ) />

<cfset mf.expect( mock.setName( mf.eqs( "Rumplestiltskin" ) ) ) />

For a complete list of matchers see the Appendix. You can also create your own matchers by implementing the IMatcher interface. There are three methods that the interface requires to be implemented.

matches( any ) This function is called to determine if the matcher's instance value matches the value passed.

asString() This function is called to display only the matcher parameter as a string. The simplest return string would be the matcher's instance value. Other matchers have more complex return strings, as in the case of the DateMatcher and NumericMatcher.

isEqual( IMatcher ) This function is called to determine if two matchers are equal. The function should return whether or not two matchers are equal based on matcher component type as well as the matcher instance values.

Thoughts on Behavioral Testing

I'm kind of amazed with the fact that we define functions or methods as the "behavior" that an object exhibits, but when we test we only care about the outcome of that behavior.

If we have a car object with a driveHome() behavior, we only care that when the function is over we've asserted ourselves into the testing easy chair. Nevermind the fact that on the driveHome() we also errand.stopAtMarket(), errand.mailBills() and friends.visit( "Bob" ), which took too much time and delivered us home too late for our favorite show.

Behavioral mock objects are just one way to be sure that is DOING what you expect it to do, not just RETURNING what you expect.

Matchers

I just ran into something that brought my unit testing to a halt. Strings. Not just any strings, but UUIDs in this case. The code under test calls a mutator method with a generated UUID, like so:

<cfset myObj.setId( createUUID() ) />

How do you set up that behavior? Well, in the current version of CFEasyMock, you can't do that. You can monkey with the MockFactory.equalsArgs() method and add an exception (not the error kind) in the logic to check for a specific string and then return a match, but that's a bit of a hack.

Enter matchers. v1.6 will add the following pattern matchers to the MockFactory object:

isA( Component ) Matches if the actual obj is a component of the type given.

matches( regexpr ) Matches if the actual value matches with the regular expression given.

startsWith( String )
contains( String )
endsWith( String ) Matches if the actual value startsWith/contains/endsWith the given value.

anyBoolean()
anyString()
anyNumber()
anyComponent() Matches any value.

The expected usage for the uuid problem could be solved in a number of way, but the matches() would provide the best comparison. Using anyString() would be too loose.

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

<cfset user = mf.createMock( "my.User" ) />

<cfset mf.expect( user.setId( mf.matches( "^[A-Z0-9]{8}-[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{16}$" ) ) ) />

<cfset mf.replay( user ) />

...

<cfset mf.verify( user ) />

Hmmmm... looks like I might have to add an anyGUID() function as well.

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.

CFEasyMock and ColdSpring/Transfer

Starting a new job and integrating cfEM into an application using ColdSpring and Transfer. The ColdSpring piece is actually quite easy, however the Transfer portion is rather interesting, since Transfer doesn't require you to actually code a component. I will keep you posted.

For ColdSpring, I have created a separate coldspring.xml file for each test file, with the naming convention "TestFile.coldspring.xml". This allows me to wire the dependencies separately for each test.

As you can see, the MockFactory can easily be wired to produce the desired mock objects.

<beans default-autowire="byName">
<bean id="mockFactory" class="easymock.MockFactory" />

<bean id="transactionAPIGateway" factory-bean="mockFactory" factory-method="createMock">
<constructor-arg name="class">
<value>cfc.am.gateway.TransactionAPIGateway</value>
</constructor-arg>
</bean>
   
<bean id="identitySyncGateway" factory-bean="mockFactory" factory-method="createMock">
<constructor-arg name="class">
<value>cfc.am.gateway.IdentitySyncGateway</value>
</constructor-arg>
</bean>
   
<bean id="transactionAPIService" class="cfc.am.service.TransactionAPIService">
<property name="transactionAPIGateway">
<ref bean="transactionAPIGateway" />
</property>
<property name="syncGateway">
<ref bean="syncGateway" />
</property>
</bean>
</beans>

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.

Using Mocks during record state

As we've started using easymock here in our CF development I've logged a few issues that have occurred when developers who aren't familiar with mocks begin using them in their unit tests.

Take the example below:

<cfcomponent name="ExampleTest">
<cffunction name="setup" returntype="void">
<cfset mf = createObject( "component", "easymock.MockFactory" ) />

<cfset mock = mf.createMock( "component.to.Mock" ) />

<cfset test = createObject( "component", "Example" ).init( mock ) />
</cffunction>

<cffunction name="testFunction" returntype="void">
<cfset mf.expect( mock.functionCall() ).andReturn( true ) />

<cfset mf.replay( mock ) />

<cfset assertTrue( test.function(), "function() failed" ) />

<cfset mf.verify( mock ) />
</cffunction>
</cfcomponent>

At first everything looks fine. The mock is created and passed to the component under test in the setup. The expected functions are called on the mock and it is set to replay. The test function runs and then the mock behavior is verified.

But what you don't see here is what happens to the mock when it is passed to the Example component in init().

<cfcomponent name="Example">
<cffunction name="init">
<cfargument name="obj" />

<cfset foo = arguments.obj.otherFunction() />

<cfreturn this />
</cffunction>
</cfcomponent>

What's this? A function call is made on the component we pass into Example.init(), in this case our mock. Since the mock has not been set to replay, the otherFunction() call is recorded. Which will cause the test to throw an error on verify.

To remedy this, the initialization of the Example component simply needs to happen in the test function, after replay.

Make sure your mocks are in replay mode before using them.

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.

More Entries

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