Monday, September 24, 2007

EasyMock testing: isolating methods with partial mocks

Today I was faced with testing a method that presented two difficulties. First, the method under test was overriding a superclass method and invoking the superclass' same-named method. Second, the method needed to invoke an unavoidable static method on a utility class. I managed to work around both difficulties with essentially the same approach, instead of invoking the difficult to mock methods directly (super.whatever() and Utility.staticMethod()), delegate to a helper method, which can then be mocked with a partial mock.

Here are the details.

The method under test overrides getAction(ActionContext context, String type, ActionConfig actionConfig).

    @Override
protected synchronized Action getAction(ActionContext context, String type, ActionConfig actionConfig) throws Exception { // NOPMD
Action action = super.getAction(context, type, actionConfig);
Class actionClass = action.getClass();
Method[] methods = actionClass.getMethods();

/*
* Determine if methods exist that need to be injected with
* beans from the ApplicationContext.
*/
for (Method method : methods) {
if (method.isAnnotationPresent(InjectBean.class)) {
InjectingCreateAction.LOG.info("Found annotation on: " + actionClass.getName());
Class< ? > clazz = method.getParameterTypes()[0];
Object bean = ApplicationContextUtils.getBean(clazz);
method.invoke(action, new Object[] { bean }); // NOPMD
}
}
return action;
}

The two methods presenting some difficulties are highlighted in yellow. A straightforward EasyMock test of this method would fail on these two methods, unless they were run in an environment where these methods could successfully execute. Since the class extends Struts' CreateAction to inject dependencies on Action classes, calling super.getAction() would require all the necessary Struts pieces to be in place when this unit test runs. That's more than we need to test this functionality.

Using EasyMock's class extension 2.2.2+, we can create a partial mock of the class under test, and use this with some simple refactoring to isolate this method to make it testable.

    @Override
protected synchronized Action getAction(ActionContext context, String type, ActionConfig actionConfig) throws Exception { // NOPMD
Action action = getActionObject(context, type, actionConfig);
Class actionClass = action.getClass();
Method[] methods = actionClass.getMethods();

/*
* Determine if methods exist that need to be injected with
* beans from the ApplicationContext.
*/
for (Method method : methods) {
if (method.isAnnotationPresent(InjectBean.class)) {
InjectingCreateAction.LOG.info("Found annotation on: " + actionClass.getName());
Class< ? > clazz = method.getParameterTypes()[0];
Object bean = getBean(clazz);
method.invoke(action, new Object[] { bean }); // NOPMD
}
}

return action;
}

/**
* Delegate method to allow for mock testing of getAction method, which would
* otherwise call a static method on {@link ApplicationContextUtils}.
* @param <T> the class of the return object (inferred)
* @param clazz the class of the requested object (explicit)
* @return an object from the ApplicationContext matching clazz
*/
public <T> T getBean(Class<T> clazz) {
return ApplicationContextUtils.getBean(clazz);
}

/**
* Delegate method to allow for mock testing of getAction method which overrides
* {@link CreateAction#getAction(ActionContext, String, ActionConfig)}.
* @param context ActionContext
* @param type String
* @param actionConfig ActionConfig
* @return The configured Action object
* @throws Exception pass-through
*/
public Action getActionObject(ActionContext context, String type, ActionConfig actionConfig) throws Exception { // NOPMD
return super.getAction(context, type, actionConfig);
}

We have created two delegate methods to do the dirty work of invoking the static method and calling the super class method. Here's how we put that to use with a partial mock.

    public void testGetAction() throws Exception {
InjectingCreateAction partialMock =
EasyMock.createMock(InjectingCreateAction.class,
new Method[] {
InjectingCreateAction.class.getMethod("getActionObject",
ActionContext.class,
String.class,
ActionConfig.class),
InjectingCreateAction.class.getMethod("getBean",
Class.class),

});

IWorkflowController bean = EasyMock.createMock(IWorkflowController.class);
SmokeTestAction test = new SmokeTestAction();
EasyMock.expect(partialMock.getActionObject(
(ActionContext) EasyMock.anyObject(),
(String) EasyMock.anyObject(),
(ActionConfig) EasyMock.anyObject())).andReturn(test);
EasyMock.expect(partialMock.getBean(IWorkflowController.class))
.andReturn(bean);

test.setWfService(bean);

EasyMock.replay(partialMock);
EasyMock.replay(bean);

Action action = partialMock.getAction(null, null, null);
assertNotNull(action);
EasyMock.verify(partialMock);
}

Creating the EasyMock with an array of Methods tells EasyMock to only mock these methods, not the entire class. The remaining calls are passed through to the underlying class. The remainder of the test is standard EasyMock stuff.

Thursday, September 06, 2007

ZopeEditManager, Safari, automation

Since Safari 3 debuted, I have been using Safari in lieu of Firefox, more and more. One feature I've missed is Firefox' ability to associate the ExternalEdit files from Plone with the ZopeEditManager. Click on the pencil, and voila, you're editing locally.



Well, I've finally figured out what to do to get this linked up in Safari. Enter Automator.



And if you're a Mac user like myself, it won't come as a surprise that it was actually quite easy.
  1. Launch Automator. You'll see a couple panes on the left (drag-src) and a pane on the right (drag-target).
  2. Your first workflow step will be Find Finder Items (the search box, top left, is a convenient way to find the workflow steps). 
  3. Select the folder where Safari will drop the .zem files (Desktop, in my case)
  4. Select "Name", "Ends with", and ".zem" for the criteria.
  5. Add the final criteria (yep, already on the last one!) - Open Finder Items.

     
  6. Nothing to configure on this workflow step, the default "Open with: Default Application" suits us just fine (note: if ZopeEditManager isn't associated with .zem files on your system, select ZopeEditManager in this drop-down).
  7. Almost done. Save your workflow as a "Folder Action" plugin. File ~ Save As Plug-in...

    Attached to Folder: should be the folder you selected in your first workflow step.
  8. Ctrl-click (i.e. right-click) on the folder you're targeting, select "Enable Folder Actions" if this is the first time you're using , Folder Actions on your system.
  9. Ctrl-click on the folder you're targeting, select Configure Folder Actions ...
  10. Click the "+" for the left pane, select your folder (e.g. Desktop)
  11. Click the "+" for the right pane, select your Automator workflow
  12. Close Folder Actions Setup, you're done!
Test it out by clicking an ExternalEdit link from Safari, should work like a charm.

Enjoy!

Wednesday, August 09, 2006

Leopard, Apple not revealing secrets

apple
In Steve Job's recent keynote, he made a point to tell listeners explicitly that not all of Leopard's features would be revealed, giving the explanation:
We don’t want our friends to start their photocopiers any sooner than they have to.
-Steve Jobs, 2006

The media has popularly taken "our friends" to mean Microsoft, and for good reason. Banners at Apple's Worldwide Developer's Conference (WWDC) berated the Redmond institution for copying Mac OS X features into Vista and generally failing to innovate.

A recent post by Andy Ihnatko at Macworld speculated that these "secret features" of Leopard might be tied to as yet unannounced hardware, which is certainly possible, even likely. But there's another possibility to consider, one that doesn't necessarily aim squarely at Microsoft's Vista.

Consider online services. Apple has made significant investments in the iTunes Music Store and .Mac, tieing in the consumer's investment in a Mac and an iPod into a larger online/digital lifestyle. In this arena, Apple faces stiff competition from Google, Yahoo, and new Web 2.0 properties like Flickr, YouTube, Picasa, Blogger, etc. With Apple's iLife suite offering "digital life" applications such as iPhoto, iWeb, iCal, and iMovie, it is easy to see how Apple's "friends" are Flickr, Blogger, Google Calendar, and YouTube (respectively).

Nevermind Microsoft's Windows Live, an online resource/community that the Redmond company will certainly be integrating into Vista when it ships.

So what kind of integrations, aggregations, and mashups might we see? I don't know, what do you think? Let your imagination loose - what would you like to see Leopard integrate with online, either with Apple's services (iTMS, .Mac) or other popular services. Post to the comments with your ideas.

Of course, much as Andy wrote, this is all pure speculation, I certainly have no more inside knowledge of what's going on at Apple than Apple's "friends" do. But, for this Apple enthusiast, at least, there's some hope that not all of Leopard's new features will require an additional monetary investment. Here's to hoping.

Wednesday, July 19, 2006

Referrals in JBoss jBPM

JBoss' jBPM doesn't really have any notion of "referring" a Task (TaskInstance). Referring a Task is analagous to an "assist". For example, a worker is assigned a Task, but the details of the Task reveal that the worker needs additional assistance or an "expert opinion" on some aspect of the Task before being able to complete the work.

Typically in this case, both the original worker and the worker rendering assistance would see the Task in the "inbox" or task list. In jBPM, however, a Task is assigned to only one worker at a time, so how can a referral be handled? One way is to use the jBPM client APIs to create a new Task on-the-fly. Typically Tasks are created when process execution enters a task-node, but jBPM also allows you to create a new Task at run-time.

For a referral, it may be that the worker rendering assistance doesn't absolutely have to complete the task for process execution to continue, so it might be a good idea to create the new Task with isBlocking set to false. Likewise, for a referral, typically the original worker is still "in control" of the Task, so isSignalling should be false. This means that when the assisting worker completes the Task (TaskInstance.end()), the current Token will not be signalled.

Tuesday, July 18, 2006

Recovering from Catastrophe

A neighbor recently had a catastrophic hard drive failure in his family's HP Pavilion laptop. These things happens, and they are usually pretty painful, unfortunately. If some foresight existed to make backups, that mitigates some of the pain, but the system still needs to be installed, configured, data restored, etc. In this case, no backups existed. From what I gather, the laptop was taken to a repair shop (Geek Squad at Bestbuy, or something along those lines) and they replaced the hard drive and installed Windows.

Upon returning home to start getting things back in order, the first order of business was getting online. Unfortunately, connecting to the wireless network was a no go. Like many folks without IT or I/S in their job description, my neighbor tried a few things without really knowing what the problem might be, but to no avail. The SMC access point had worked fine for two years, so something with the laptop must still be wrong.

So, this afternoon while standing in the alley watching our kids play, he asked if I had any experience setting up wireless access points. Indeed, I have set up many wireless systems, from a PC-based access point from Linux/Mac/Windows, to Airport, Linksys, Cisco, and Netgear hardware access points.

So, after the kids were in bed, I headed over. After some initial poking around, I was baffled that XP's network connections control panel didn't even show a wireless network adapter. After getting some of the back-story above, I figured that the repair shop had just installed Windows, but not any of the drivers for the HP Pavilion hardware. HP's site is quite good, so a few clicks later, the wireless adapter was visible in the systray.

At that point, getting the SMC access point setup again was simple. Note: don't hit the reset button on your hardware unless you really know what you're doing. If you know you're going to have to re-configure your hardware when you hit reset, feel free. If you think hitting reset might get things working again - please refrain. Thankfully, Amazon started linking to PDF manuals for many electronics items over the last couple years - so a quick search got us on track for configuring the access point from zero.

Lesson: most systems these days need special drivers not bundled with Windows, so if a Windows reinstall is just what the doctor ordered, be sure to follow-up with installing your hardware's drivers. Ideally, you can mention this to your repair shop and they'll do what they can do get things setup for you. Also, avoid the reset button in most troubleshooting situations.

Friday, July 14, 2006

Best design: flexible or simple?

Should software be designed for flexibility to meet future needs or should the software be designed as simply as can be, to meet current needs and requirements? I would argue the latter, which probably puts me in the camps of Fowler et al.

Here's why: if future needs are uncertain, trying to design for future flexibility may well build the software into a cage rather than yielding the desired flexibility. Sure, the software is "flexible" if the future needs match the predicted future needs built into the design.

On the other hand, if the software is kept simple, meeting the known requirements and designed well (following OO principles and such), when new needs surface, the software can be amended to meet those needs. Since there's been no predetermined design for this new need, this should provide the maximum flexibility.

This embodies the spirit of iterative, incremental development. Done right, IID should be agile by nature, not by following some certain prescribed agile methodology such as SCRUM or XP. I think many projects quickly grasp iterative development, but incremental development seems a more difficult concept to understand.

Understand this - if the goal is to predict future needs, with incomplete current understanding, then even if its called "designing for flexibility" - it really is little more than waterfall development. Incremental development isn't just about building the system in stages or phases. It is also about determining the needs, the design, the implementation, and the testing methodology in iterations. Of course, this means the project starts with an incomplete understanding of the details of the final system. That's no fault of IID - that's just the reality of things.

Wednesday, July 12, 2006

MVC, as simple as HTML?

I recently got to thinking about MVC as it relates to GWT, AJAX, the Web, etc. Of course, there are many MVC(2) frameworks out there - Struts, Webworks, Spring MVC, etc. But, there's a simple MVC, I think.
  • Model :: XHTML
  • View :: CSS
  • Controller :: JavaScript
Let's think about this for a second. If XHTML is the model, it contains the state of "system" (the page in this case). If the XHTML is done correctly, it contains only structural/semantic markup, right? No presentation information. This is just the data, and that's the model, pretty simple.

Now, it makes sense that CSS is the view. It is the presentation of the data. As most people have seen with the CSS Zen Garden, the CSS can dramatically change the view/presentation of the data. CSS can also do minimal interaction (e.g. :hover). CSS is the view, that seems pretty simple, too.

Now the controller. JavaScript? Yep, I think so. If by JavaScript we take into account the browser itself, JavaScript is "observing" the view by monitoring mouse movements (onhover, onblur, etc.) and many other things as well. The controller then will modify the model (AJAX, modifying innerHtml, etc.). Changes in the model will automatically be reflected in the view (e.g. if JavaScript changes a node's class).

The more I think about it, the more I am satisfied that XHTML + CSS + JavaScript is indeed a simple example of MVC.