Wednesday, August 13, 2008

Grails Tests: Cannot send redirect - response is already committed

When testing controllers, if you make more than one call to a action/closure you may get the error: "Cannot send redirect - response is already committed"
But if you want to call one action to modify the database and then test the results in a subsequent call, if you put that call in a different test method, the prior test's transaction will probably be rolled back. So you want to make both action/closure calls in the same test method.

To get around this mess I simply MOPed it up (with help from Venkat Subramaniam's book "Programming Groovy"):


controller.metaClass.redirect = { Map args -> return args}


Note that the action of the redirect will not be invoked.
Also note that I am MOPing an instance, not the Class.

Grails Controller Tests: ModelAndView

I just joined the team at AlterThought.com and my first task was to revitalize the integration test for a bunch of controllers. But Grails no longer seems to have the modelAndView as a standard attribute of a controller. And we have assertions like:


assert controller.modelAndView.model.contractList.size() > 1


My solution was to override the controller's render method and stuff in a modelAndView to the controller myself:


import org.springframework.web.servlet.ModelAndView

class IntegrationTestUtil {
void overrideRender(controller) {
def dir = controller.class.name.replaceAll(/Controller/, '')
dir = dir[0].toLowerCase() + dir.substring(1)
def originalRender =
controller.metaClass.getMetaMethod("render", [Map] as Class[])
controller.metaClass.render = { Map args ->
if(args.view && args.model) {
delegate.metaClass.getModelAndView = {->
new ModelAndView("/${dir}/${args.view}", args.model)
}
} else {
originalRender.invoke(delegate, args)
}
}
}
}


My controllers then invoke the overrideRender method in setUp and the test run fine.

Note the override only changes render invocations that pass a map with model and view entries.

Tuesday, April 15, 2008

Grails UI for Integer Dates

Legacy tables often have dates kept in integer, zoned, or packed fields. This is unfortunate because the UI developer has to do all the validation and formatting of the numeric value. But there's a simple solution for Grails applications: create getter/setter methods to simulate a Date attribute.

Grails' generate-view will use the g:datePicker tag in the create and edit GSPs. Typically I replace g:datePicker with the RichUi tag.

In the following example, the domain has an integer sales date, in month/day/year format but getSaleDate() and setSaleDate() surfaces a Date attribute. The trick is to not present the integer date attribute for update (otherwise the value will overwrite the change made by setSaleDate()).


class Sale {
String last
int saleMDY
BigDecimal amount

Date getSaleDate() {
Calendar cal = Calendar.getInstance();
if (!saleMDY) {
return new Date()
}
int year = (saleMDY % 100)
int day = saleMDY / 100
int month = day / 100
day %= 100
cal.set((2000+year), (month-1), day)
return cal.time
}
void setSaleDate(Date date) {
Calendar cal = Calendar.getInstance();
cal.setTime(date)
int day = cal.get(Calendar.DAY_OF_MONTH)
int month = cal.get(Calendar.MONTH) + 1
int year = cal.get(Calendar.YEAR) % 100
saleMDY = (month * 10000) + (day * 100) + year
}
}

Friday, February 22, 2008

Integration testing for Grails controllers

I've been negligent in writing integration tests for my controllers, mostly because I ran into some issues and put off figuring it out. So while on a train I wrote a simple 24-line controller with action-closures that used the 5 basic operations most in my applications:

  1. render

  2. rended JSON

  3. return map

  4. redirect

  5. return string


And I threw in a service for good measure, and then wrote integration tests.
Here's a couple of points to remember:

  • When a map is returned, the map is available directly as the return value from the action

  • When JSON is returned, use the controller.response.contentAsString

  • When a String is returned, it is available directly as the return value from the action

  • When the action does a render, use the controller's ModelAndView's model property to test model data. Note that the view did not actually render but you can test the viewName property value. Also note that what you can't seem to be able to do is see how the page would ultimately be rendered.

  • When the action does a redirect, check the url with the controller's MockHttpServletResponse

  • To use a service, the test has to interject the service. Note there may be other things that Grails normally interjects for you that you may have to manually set



Here's the domain:

class Person {
String firstName, lastName, email
String toString() {"$id $firstName $lastName <$email>" }
}

Here's the controller:

class PersonController {
def list = {
[ personList: Person.list( params ) ]
}
def showWithReturnMap = {
def person = Person.get( params.id )
if(!person) {
flash.message = "Person not found with id ${params.id}"
redirect(action:list)
}
else { return [ person : person ] }
}
def showWithReturnString = {
return Person.get( params.id ).toString()
}
def showWithRender = {
render view:'show', model:[ person : Person.get( params.id ) ]
}
def showWithJSON = {
render Person.get( params.id ) as JSON
}

MathService mathService
def usesService = {
return mathService.add(params.a, params.b).toString()
}
}


And here's the integration test:

class PersonControllerTests extends GroovyTestCase {
// when true, Grails does a rollback after each test method
boolean transactional = false

void setUp() {
new Person(lastName:'Denoncourt', firstName:'Don', email:'ddenoncourt@cassevern.com').save()
}

void testShowWithReturnMap() {
def controller = new PersonController()
controller.params.id = 1
def model = controller.showWithReturnMap()
assertFalse "Person was found",
controller.flash.message ==~ /Person not found with id 1/
assertNotNull "Person returned as model", model
assertEquals "Person is a Denoncourt",
model.person.lastName, 'Denoncourt'
}

/* When a String is returned,
* it is available directly as the return value
* from the action
*/
void testShowWithReturnString() {
def controller = new PersonController()
controller.params.id = 1
def model = controller.showWithReturnString()
assertEquals "Don Denoncourt found",
model, "1 Don Denoncourt "
}

/* When the action does a render,
* use the controller's ModelAndView's model property
* to test model data
* Note that the view did not actually render but you can test
* the viewName property value
* What you can't seem to be able to do is see how the page
* would ultimately be rendered.
*/
void testShowWithRender() {
def controller = new PersonController()
controller.params.id = 1
controller.showWithRender()
assertTrue "Person was found",
!( controller.flash.message =~
/Person not found with id/
)
assertNotNull "Person returned in ModelAndView",
controller.modelAndView.model
assertEquals "Person is a Denoncourt",
controller.modelAndView.model.person.lastName,
'Denoncourt'
assertEquals "view should be show",
controller.modelAndView.viewName,
"/person/show"
}

/* When the action does a redirect, check the url with the controller's MockHttpServletResponse */
void testShowButRedirected() {
def controller = new PersonController()
controller.params.id = 9898
def model = controller.showWithReturnMap()
assertNotNull "Should have a flash message", controller.flash
assertTrue "Person should not be found",
controller.flash.message ==~
/Person not found with id 9898/
assertEquals "/person/list",
controller.response.redirectedUrl
}
/* When JSON is returned,
* use the controller.response.contentAsString
*/
void testShowWithJSON() {
def controller = new PersonController()
controller.params.id = 1
controller.showWithJSON()
assertEquals("""{"id":[1,"class":"Person",
"email":"ddenoncourt@cassevern.com",
"firstName":"Don","lastName":"Denoncourt]"}""",
controller.response.contentAsString)
}

/* to use a service,
* the test has to interject the service
*/
MathService mathService
void testUsesService() {
def controller = new PersonController()
controller.mathService = mathService
controller.params.a = 2
controller.params.b = 2
def model = controller.usesService()
assertEquals "2+2=4", model, "4"
}
}

Saturday, February 16, 2008

GORM insert and update auto-set of createUser and updateUser

Our company has a standard of using the following attributes on all database tables:

String createUser
Date createDate
String updateUser
Date updateDate


But we did not want to put code in all controller's to set these fields on insert or update. Grails has a nice facilities where you specify code in the beforeInsert and beforeUpdate closures to set the dates:

def beforeInsert = {
createDate = new Date()
}
def beforeUpdate = {
updateDate = new Date()
}

Grails will call these closures automatically. But the issue is setting the createUser and updateUser. What we did is create an abstract base class (concrete makes Grails look for a table called base):

abstract class Base {

String createUser = ''
Date createDate = new Date()
String updateUser = ''
Date updateDate = new Date()

def beforeInsert = {
setCreateUsername() // must be injected in login controller
createDate = new Date()
delegate.createUser = this.createUser
delegate.updateUser = this.updateUser
}

def beforeUpdate = {
setUpdateUsername() // must be injected in login controller
updateDate = new Date()
delegate.updateUser = this.updateUser
}
}


Each of our domain classes extended Base (so they did not have to specify the 4 fields.)
Then, when the user login is handled by the Login controller, we inject the setCreateUsername and updateUsername "methods" as closures that set the username with the HttpSession value:


def user = User.findWhere(userId:params.userId, userPwd:params.userPwd)
session.user = user
Base.metaClass.setCreateUsername = {
createUser = session.user.userId
updateUser = session.user.userId
}
Base.metaClass.setUpdateUsername = {
updateUser = session.user.userId
}


And the four columns are automatically set on all insert and updates.

Grails WebFlow and explicit events

Grails WebFlow enables the ability to keep a context specific to a set of pages. Grails webflow is built on top of Spring webflow but, as always, Grails adds ease of use with a DSL.
When do you need webflow? Whenever you find yourself stuffing things in the session context.
One side note, if you are putting something in the session that is used in the very next request, use the flash context, as it retains items in the flash scope for two request cycles (unless the item is refreshed on the second request.

A webflow is contained in one controller. Reading existing webflows is fairly easy but, until you become comfortable with a few terms, writing webflows can be problematic. Things that you need to understand are Flow Scopes, View States, and Action States.

To get started with webflow, read, in its entirety, the documentation at WebFlow and then download and play with the book-flow
sample application. If you do not know how to check out a Subversion, email me and I'll walk you through it.

Grails WebFlow seems to be limited somewhat in GSP tags: g:submitButton and g:link. Remember that in the Grails RESTful architecture, URLs follow the domain/controller/action convention to identify the controller and closure (the action) to handle the request. WebFlows adds the concept of an event. Strangely the g:submitButton's name attribute defines the event but the g:link uses the more obvious event attribute. But what if you want to use an image button or invoke a request from a JavaScript event?

If you look at the HTML generated from the g:submitButton or g:link tags you will see a request parameter with a name the is prefixed with _eventId_. The string that follows the second underscore identifies the WebFlow event.
So for an application that requires an image, a click of which should be handled by the edit event of a webflow:

<input class="edit" name="_eventId_edit"id="_eventId_edit"
value="Edit" type="image"
src="/FAB/images/skin/database_edit.png"
/>

To have a select list that runs the createMop (method of payment) event when a user select a item from the method of payment types list:

<g:select id='mopType' name='mopType'
onchange="location='/FAB/document/documentWork?_eventId_createMop=createMop&'+Form.serialize(\'docLogForm\');"
from='${(BootStrap.mopTypes.entrySet().value)}'
keys='${(BootStrap.mopTypes.entrySet().key)}'
/>

Notice the use of the serialize method of the Form object. The serialize method comes from the prototype JavaScript library.