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.

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>

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>

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.

Named vs Unnamed Arguments in CF functions

ColdFusion gives developers a great deal of flexibility when creating functions. Not all arguments have to be known at runtime by declaring them with a tag. Any additional arguments passed get a number key relative to their position (if they aren't passed in by name or via an argumentcollection).

So for function foo...

<cffunction name="foo">
<cfargument name="bar" /.
...
</cffunction>

called by

<cfset foo( 10, 20, 30, 40 ) />

Would yield an argument struct of: bar = 10, 2 = 20, 3 = 30, 4 = 50

If you call foo like the following:

<cfset foo( bar=10, var1=20, var2=30, bar2=40 ) />

you would get an argument struct of: bar = 10, var1 = 20, var2 = 30, bar2 = 40

At first I thought this was a great concept. Less typing, not constrained to a particular implementation, etc. Then I got to thinking. This could really turn into a slippery slope.

Functions that don't have a clear set of arguments can easily end up not having a clear definition of purpose. (Not to mention difficult to debug!) They could start to take on more and more behavior and then lose any resemblance of reusability.

Functions that perform one specific behavior will have a known set of inputs. Those input values may vary, but the inputs themselve are always known.

There are some instances where unnamed arguments does make things a lot easier. There's even an example of that within CFEasyMock itself, in the MockFactory.replay() method (and reset() and verify()). It is simply easier to pass in a series of components rather than make an array out of them first. (Which would be the most prudent way to do it.) However, there is still the chance that someone passes in the wrong object, but that wouldn't be caught until runtime anyway.

It's a double edged sword. There are definite benefits, but it should be used sparingly with good reason, as opposed to a general coding practice.

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