There is lots of contradictory information on the web, even in the Grails docs, on testing Grails controllers. The biggest issues are understanding the ControllerUnitTestCase, and what it does and does not do.
First, here's a controller I'm developing that will back a JQuery JTree component. The first method will take a parent path name, and return a JSON representation of the iRODS file system for the parent path using some new Jargon methods that are meant to assist in Swing and JQuery tree development.
A few things to note. The new Jargon libraries are meant to provide simple POJO's that represent various domain objects within iRODS. There also is an idea of Access Objects, that roughly equate with the DAO pattern. iRODS is not a database, so the mapping is not the same, but to me, the idea of Access Objects for iRODS at least gives some comfort in familiarity. One access object I'm developing is meant to assist in tree depictions and searching on file and collection paths to support such elements in typical web and Swing GUI applications. We'll use that Access Object.
package org.irods.mydrop.controller
import org.springframework.security.core.context.SecurityContextHolder;
import org.irods.jargon.core.pub.*;
import org.irods.jargon.core.connection.*;
import org.irods.jargon.core.exception.*;
import grails.converters.*
/**
* Controller for browser functionality
* @author Mike Conway - DICE (www.irods.org)
*/
class BrowseController {
IRODSAccessObjectFactory irodsAccessObjectFactory
IRODSAccount irodsAccount
/**
* Interceptor grabs IRODSAccount from the SecurityContextHolder
*/
def beforeInterceptor = {
def irodsAuthentication = SecurityContextHolder.getContext().authentication
if (irodsAuthentication == null) {
throw new JargonRuntimeException("no irodsAuthentication in security context!")
}
irodsAccount = irodsAuthentication.irodsAccount
log.debug("retrieved account for request: ${irodsAccount}")
}
/**
* Display initial browser
*/
def index = { }
/**
* Render the tree node data for the given parent.
*
* Requires param 'dir' from request to derive parent
*
*/
def loadTree = {
def parent = params['dir']
log.info "loading tree for parent path: ${parent}"
def collectionAndDataObjectListAndSearchAO = irodsAccessObjectFactory.getCollectionAndDataObjectListAndSearchAO(irodsAccount)
def collectionAndDataObjectList = collectionAndDataObjectListAndSearchAO.listDataObjectsAndCollectionsUnderPath(parent)
log.debug("retrieved collectionAndDataObjectList: ${collectionAndDataObjectList}")
render collectionAndDataObjectList as JSON
}
}
I've wired in my Spring security objects as described in an earlier post. My interceptor in my controller grabs the saved authentication token from the Spring Security layer and extracts the IRODSAccount object. In the loadTree method, this IRODSAccount object is used with a factory injected into my controller to get a CollectionAndDataObjectListAndSearchAO. My controller uses the method that takes a parent path, and returns JSON objects that represent iRODS files and collections under that parent path.
The approach taken in Jargon seems to map fairly easily into Grails apps, that's a good sign. This shows that quick development of arbitrary interfaces on top of iRODS will become easier with the newer Jargon libraries. Yay!
So, the problems really began with trying to test. First, let me share a working test, then I'll talk about some of the problems I ran into:
package org.irods.mydrop.controller
import grails.test.*
import java.util.Properties
import org.irods.jargon.core.connection.IRODSAccount
import org.irods.jargon.core.pub.IRODSAccessObjectFactory
import org.irods.jargon.core.pub.IRODSFileSystem;
import org.irods.jargon.core.pub.io.IRODSFile
import org.irods.jargon.core.query.CollectionAndDataObjectListingEntry
import org.irods.jargon.testutils.TestingPropertiesHelper
import org.irods.jargon.testutils.filemanip.FileGenerator
import org.irods.jargon.testutils.TestingPropertiesHelper
import org.irods.jargon.spring.security.IRODSAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import grails.converters.*
class BrowseControllerTests extends ControllerUnitTestCase {
IRODSAccessObjectFactory irodsAccessObjectFactory
IRODSAccount irodsAccount
Properties testingProperties
TestingPropertiesHelper testingPropertiesHelper
IRODSFileSystem irodsFileSystem
protected void setUp() {
super.setUp()
testingPropertiesHelper = new TestingPropertiesHelper()
testingProperties = testingPropertiesHelper.getTestProperties()
irodsAccount = testingPropertiesHelper.buildIRODSAccountFromTestProperties(testingProperties)
irodsFileSystem = IRODSFileSystem.instance()
irodsAccessObjectFactory = irodsFileSystem.getIRODSAccessObjectFactory()
def irodsAuthentication = new IRODSAuthenticationToken(irodsAccount)
SecurityContextHolder.getContext().authentication = irodsAuthentication
}
protected void tearDown() {
super.tearDown()
}
void testBrowseNoLogin() {
controller.params.dir = "/"
controller.irodsAccessObjectFactory = irodsAccessObjectFactory
controller.irodsAccount = irodsAccount
controller.loadTree()
def controllerResponse = controller.response.contentAsString
def jsonResult = JSON.parse(controllerResponse)
assertNotNull("missing json result", jsonResult)
}
}
One of the first sources of confusion to me were documents that seemed to imply that running the grails command create-controller would put the relevant test in the integration tests directory. It didn't, it placed the test in the test/unit directory. Go figure. Maybe I'm missing something, but I'm taking what Grails does at it's word, so to speak. I tried moving the test to the integration test directory, but found that this resulted in the setup methods not running. Fine...I'll just forget this rabbit-hole.
Second, I had the hardest time setting up the params. My controller expects a request param of 'dir', with my test specifying the root '/' directory. I kept getting a "No such property: params" error when I tried doing the
controller.params.dir = "/"
test setup. It turns out that the ControllerUnitTestCase is wanting to wire in and extend your controller for you, creating a 'controller' variable automatically, with the test target controller defined by convention using the test name. So instead of creating my controller in a def using the 'new' keyword, I had to let the ControllerUnitTestCase do it for me.
I also ran into the issue of irodsAccount being null. The ControllerUnitTestCase does not run my interceptor for me. I was getting a null irodsAccount, and realized that I had to manually inject that variable in my test code.
Well, I was almost home, I kept getting a 'null' for my controller variable in my test case. Doh...I had plugged a bunch of code into setUp() in my test case, and I had to go back and add the super() call in my test case so that ControllerUnitTestCase could do it's magic on the controller variable. So now it works! There's still lots of refactoring I want to do in the controller, and I need to do things like parse and test the actual JSON response, but these are the details to work out now that I can get the stupid test cases to run.
Now that's progress...hopefully that will help you avoid some of my own frustrations. Here's to a New Year and new adventures in coding. I think Grails/Groovy feels as immature as most new dynamic scripting languages, but at the same time, I really feel like, combined with the new Jargon libraries, I'm well on my way to a rapid web development stack on top of iRODS. That's a good thing!
No comments:
Post a Comment