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

Chugging away...

Ryan Wood and I have been working through a few issues. We've uncovered and fixed a bug with the expect() method. There's a new zip with the latest version on the project site, v1.0.002

Ryan has also proposed a solution for allowing CFEasyMock to work with injected functions. I'll be working on getting that completely integrated this weekend.

Thank you Ryan!

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.

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.

CFEasyMock finds a home

The company I currently work for is primarily a java shop. The java development side of the house already has a lot of process around TDD and unit testing.

One of the advantages they have in their TDD and unit testing frameworks is a java package called EasyMock. It allows the developer to create tests that can verify behavior of functions. Normally, when we right unit tests we are only asserting some variable state. What actually happens in the function to change that variable state is a mystery.

Behavioural mocks allow for interactions between the function under test and the collaborator component(s) to be checked for expected behavior.

I ported over the EasyMock package for two reasons. 1) The CF development processes are starting to take on more agile development aspects. We need to define function behavior during testing. 2) We also needed mocks that work under CFMX 7.x, as we don't run 8.x yet.

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