Created
March 22, 2011 20:01
-
-
Save bendoerr/881935 to your computer and use it in GitHub Desktop.
Unit test harness for Grails Web Flows
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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