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.

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.

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