Skip to content

Instantly share code, notes, and snippets.

@bendoerr
Created March 22, 2011 20:01
Show Gist options
  • Save bendoerr/881935 to your computer and use it in GitHub Desktop.
Save bendoerr/881935 to your computer and use it in GitHub Desktop.
Unit test harness for Grails Web Flows
import grails.test.ControllerUnitTestCase
import org.apache.commons.lang.StringUtils
/**
* Provide support for unit testing Grails web flows inside of controllers.
* Extending ControllerUnitTestCase which will provide us with much
* of the needed mocking around properties and methods that Grails
* provides. Just as ControllerUnitTestCase does it will determine the
* controller based on the test name as well as the web flow to test.
*
* @author Benjamin R Doerr
*/
class WebFlowUnitTestCase extends ControllerUnitTestCase {
protected String webFlowName
protected Closure webFlowClosure
protected Map webFlow
protected Map mockFlowScope
protected Map mockConversationScope
protected String stateTransition
String lastEventNameOnActionCall
/**
* Creates a new test case for the controller and web flow that is in the
* same package as the test case and has the same prefix before "Controller"
* in its name and web flow has the prefix before "Flow" . For example,
* if the class name of the test were
* <code>org.example.TestControllerTestWebFlowTests</code>, this constructor
* would mock <code>org.example.TestController</code> and the
* <code>testFlow</code> of the controller.
*/
WebFlowUnitTestCase() {
super()
def matcher = getClass().name =~ /Controller(.*?)WebFlow/
if (matcher) {
webFlowName = "${StringUtils.uncapitalise(matcher[0][1])}Flow"
} else {
throw new IllegalArgumentException("Cannot find matching web flow for this test.")
}
}
/**
* Creates a new test case for the given controller class and
* web flow.
*/
WebFlowUnitTestCase(Class controller, String webFlowName) {
super(controller)
this.webFlowName = webFlowName
}
protected void setUp() {
super.setUp()
webFlowClosure = controller."$webFlowName"
webFlow = WebFlowUnitTestSupport.translate(webFlowClosure, { lastEventNameOnActionCall = it})
}
protected Object newInstance() {
Object instance = super.newInstance()
mockFlowScope = [:]
mockConversationScope = [:]
stateTransition = null
instance.metaClass.getFlow() {
return mockFlowScope
}
instance.metaClass.getConversation() {
return mockConversationScope
}
instance.metaClass.methodMissing {String name, Object args ->
handleWebFlowMethodMissing(name, args)
}
return instance
}
/**
* Registers the end transition state of a web flow if it is returned as
* <code>return success()</code>
*/
protected Object handleWebFlowMethodMissing(String name, Object args) {
if (lastEventNameOnActionCall && webFlow."$lastEventNameOnActionCall".on."$name") {
stateTransition = name
return name
}
throw new MissingMethodException(name, this.class, args)
}
}
class WebFlowUnitTestSupport {
static final String BUILD = "build"
Map flowMap
Map currentEvent
String currentOnEvent
String currentEventName
Boolean done = false
Closure setEventOnActionCallback
static Map translate(Closure closure, Closure setEventOnActionCallback) {
return new WebFlowUnitTestSupport(setEventOnActionCallback)."$BUILD"(closure)
}
WebFlowUnitTestSupport(Closure setEventOnActionCallback) {
this.setEventOnActionCallback = setEventOnActionCallback
}
Object invokeMethod(String name, Object obj) {
Object[] args = obj.class.isArray() ? obj : [obj] as Object[]
if (!done) {
if (name == BUILD) {
return doBuild(name, args)
}
if (!isFlowInitialized()) {
throw new IllegalArgumentException("Call to [$name] not supported here.")
}
if (!isCurrentEventInitialized()) {
return handleEvent(name, args)
}
}
MetaMethod metaMethod = metaClass.getMetaMethod(name, args)
if (metaMethod == null) {
throw new MissingMethodException(name, this.class, args)
}
return metaMethod.invoke(this, args)
}
Object invokeClosureNode(Object args) {
Closure callable = args
callable.delegate = this
callable.resolveStrategy = Closure.DELEGATE_FIRST
return callable()
}
Map doBuild(String name, Object[] args) {
if (isFlowInitialized()) {
throw new IllegalArgumentException("Call to [$name] not supported here. Must call $BUILD first.")
}
flowMap = [:]
invokeClosureNode(args[0])
done = true
return flowMap
}
void handleEvent(String name, Object[] args) {
currentEvent = [:]
currentEventName = name
if (args.length > 0 && args[0] instanceof Closure) {
invokeClosureNode(args[0])
}
flowMap.put name, currentEvent
currentEvent = null
currentEventName = null
}
Boolean isFlowInitialized() {
return flowMap != null
}
Boolean isCurrentEventInitialized() {
return currentEvent != null
}
void action(Closure actionClosure) {
currentEvent.action = wrapWithEventName(actionClosure)
}
Object on(String event) {
return on(event, null)
}
Object on(String event, Closure closure) {
if (!currentEvent.on) {
currentEvent.on = [:]
}
currentOnEvent = event
currentEvent.on."$event" = [:]
if (closure) {
currentEvent.on."$event".action = wrapWithEventName(closure)
}
return this
}
void to(String state) {
if (!currentOnEvent) {
throw new IllegalArgumentException("Call to [to] not supported here. Must call on first.")
}
currentEvent.on."$currentOnEvent".to = state
currentOnEvent = null
}
void subflow(Object subflow) {
currentEvent.subflow = wrapWithEventName(subflow)
}
Closure wrapWithEventName(Closure action) {
String event = currentEventName
Closure wrap = {->
setEventOnActionCallback.call(event)
return action()
}
return wrap
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment