Saturday, November 14, 2009

Writing a simple easyb plugin

NOTE: I wrote this article up, published it, then found this PluginAPI wiki article on the easyb site. The only difference I saw was the package names of the classes involved. Just in case this article provides a bit of clarification, I'll keep it around. . . .

The easyb BDD framework provides a ton of great functionality out of the box, but as with anything, sometimes there are features that you want that the developers can't predict. The easyb developers realized this and created a framework for adding plugins to extend easyb and make it do what you need it to.

Plugins for easyb implement the EasybPlugin interface, basically providing behavior before and after easyb "events" (by "events", I mean when easyb runs somethng like "given" or "scenario"). The EasybPlugin interface looks like this:

package org.easyb.plugin

interface EasybPlugin {
String getName()
def beforeScenario(Binding binding)
def afterScenario(Binding binding)

def beforeGiven(Binding binding)
def afterGiven(Binding binding)

def beforeWhen(Binding binding)
def afterWhen(Binding binding)

def beforeThen(Binding binding)
def afterThen(Binding binding)

def beforeStory(Binding binding)
def afterStory(Binding binding)

void setClassLoader(ClassLoader classLoader)
}


Most of the time, however, you probably don't need to provide actions for *every* event, so instead of implementing EasybPlugin, you can just extend BasePlugin which provides no-op implementations of the "event" methods, overriding whichever methods you need.

Let's get into an example. Let's say we want to extend easyb to mimic injecting resources into a story run with the Grails easyb plugin (see my post here for the story behind that). The first thing we want to do is set up our Groovy class and implement the one abstract method in BasePlugin, getName():

class GrailsInjectPlugin extends BasePlugin {
public String getName() { "grails-inject" }
}


The getName() method serves two purposes: first, in the easyb story, you'll have a "using" line with the value that getName() returns, signaling easyb to use the plugin. Second, easyb will use this name to lookup the plugin via its PluginLocator class.

At this point, we could compile and include the plugin, and it would be valid, but it wouldn't actually do anything. To extend easyb's behavior, we need to override one of the "event" methods. In the case of injecting Grails resources, we want to set up our resources before an entire story executes (similar to how they'd be initialized in a Grails test, def-ing them at the beginning of a test class). We'll override the beforeStory() method:

def beforeStory(Binding binding) {
binding.inject = { beanName ->
binding."${beanName}" =
ApplicationHolder.application.mainContext.getBean(beanName)
}
}


The "event" methods get passed a Binding implementation which basically is what easyb will use to look up values, methods, and the like that it encounters in its stories. In our beforeStory() implementation, we're telling the binding that it now has an "inject" method that takes a bean name as an argument. The method looks up that bean in the Grails Spring context, then, in the binding, associates the bean with the passed in bean name.

The final code for the plugin class looks like this:

package com.agileice.easyb.plugin

import org.codehaus.groovy.grails.commons.ApplicationHolder
import org.easyb.plugin.BasePlugin

class GrailsInjectPlugin extends BasePlugin {

public String getName() { "grails-inject" }

def beforeStory(Binding binding) {
binding.inject = { beanName ->
binding."${beanName}"=
ApplicationHolder.application.mainContext.getBean(beanName)
}
}
}


It's all fairly simple and straightforward . . . the easyb developers did a great job in making plugin development easy.

The next step in getting the plugin into your application is to include it in a JAR file. In addition to our plugin classes, we need to include some information in the JAR's META-INF directory. When easyb is looking for plugin implementations, it uses the sun.misc.Service class, so we need to include a file named org.easyb.plugin.EasybPlugin in the JAR's META-INF/services directory to tell the Service class that we provide an implementation of EasybPlugin. That file will contain the name of our implementation class:

com.agileice.easyb.plugin.GrailsInjectPlugin # inject Grails beans


At this point, we can compile our plugin, JAR up our classes and the META-INF information, and with the JAR on the classpath, run our easyb stories. To use the plugin in a story, we include a "using" line with the name of our plugin (the value returned by the getName() method). With the plugin included, we can use the keywords we've defined:

using "grails-inject"

inject "grailsApplication"
inject "someService"

scenario "Grails App injection", {

given "injected Grails resources"
then "the Grails application should not be null", {
grailsApplication.shouldNotBe null
}
and "the service instance should not be null", {
someService.shouldNotBe null
}
}


Now, when the story runs, easyb should pick up our plugin, handle our behavior, and execute our scenarios. Note, the above examples should work with the version of easyb included with the Grails easyb plugin (marked 0.9.7). If you're working with an older version (say, from a Maven repository where the latest version is 0.9.5), you'll probably have to implement all of EasybPlugin yourself (since it looks like BasePlugin doesn't exist there), changing the "def" for each method to "void". Once you do that, everything else should work the same.

1 comment:

Andy said...

I'm glad you found it easy to make a plugin!! Thanks for your support!