Thursday, 23 September 2010

Stubbing g.message

It's easy to mock grails taglibs using gmock and hamcrest. You can do something like...

...
def g

void setup() {
g = mock()
mock(tagLib).getG().returns(g).stub()
}

void someTest() {
g.message(hasEntry('code', 'foo.bar')).returns('FOO BAR')
String result
play {
result = tagLib.someMethod([:]).toString()
}
assert result.contains('FOO BAR')
}
}

But when you've got a lot of calls to g.message this can become noisy and usually ends up being repeated in multiple tests. One option is to be relax the argument matching and switch to using a stub.

g.message(instanceOf(Map)).returns('FOO BAR').stub()

This is fine for the tests where you don't really care about the message, but too loose for the ones you do. I'm playing with the an alternative which gives you both stubbing and the option of explicitly asserting g.message was called with the correct arguments. Be warned though it may raise a WTF exception on first glance.

...
def g

void setup() {
g = mock()
mock(tagLib).getG().returns(g).stub()
stubMessages(g)
}

void someTest() {
String result
play {
result = tagLib.someMethod([:]).toString()
}
assert result.contains('code:foo.bar')
}
}

class MessageMatcher extends BaseMatcher {

String code = ''

static void stubMessages(def g) {
MessageMatcher matcher = new MessageMatcher()
g.message(matcher).returns(matcher).stub()
}

boolean matches(Object o) {
code = ((Map) o).containsKey('code') ? o.code : ''
return true;
}

void describeTo(Description description) { }

String toString() {
return "code:${code}"
}
}

The tricky bit was getting the g.message stub to return a value derived from the matcher arguments. I couldn't just do

g.message(matcher).returns(matcher.code).stub()

because matcher.code won't have a value at this point. The solution is to override the matcher's toString() method to return the code and rely on groovy / grails invoking toString() when adding the matcher to the output. This certainly violates the rule of least surprises, but I think I'm OK with that if it reduces duplication and the noise level of my tests - at least until I find a better way.

Friday, 17 September 2010

Clean TagLib Tests

Despite my best efforts I've always found it hard to write clean taglib tests. Now thanks to Spock and GroovyShell things are getting easier...

void setup() {
mockDomain Invoice
}

def "Attachments icon has correct markup"() {
given:
Invoice invoice = new InvoiceBuilder().buildAndSave()

when:
renderAttachmentsIcon([target: invoice])

then:
valueOf('img.@id') == "toggle-attachments-${invoice.id}"
valueOf('script') == "\$('#toggle-attachments-${invoice.id}').bind('click', Books.attachments.toggle);"
}

Because I want to use GPath make assertions about the resulting HTML I've overriden TagLibSpec's methodMissing closure as follows...

def methodMissing(String name, args) {
String html = super.methodMissing(name, args)
createDocument(html)
}

void createDocument(String html) {
String xml = "<results>${html}</results>"
document = new XmlSlurper().parseText(xml)
}

And added helper methods for evaluating GPath expressions...

String valueOf(String gPath) {
evaluate(gPath).text()
}

GPathResult evaluate(String gPath) {
new GroovyShell(new Binding(document: document)).evaluate("document.${gPath}")
}

There's a little bit more to this story unfortunately...

Firstly my taglibs use the MarkupBuilder and when run from my Spock test this only outputs the opening tag of the first element I genererate. I haven't had a chance to look into this yet, but a workaround is to add "out << '' to the end of the taglib method

Secondly Spock interactions aren't yet as powerful as gmock, so I usually end up adding code to (g)mock grails taglibs.

Thirdly another one of my tests outputs &nbsp; in the HTML which causes the XML parsing to barf. The solution is to map the &nbsp entity to a known character (in this case underscore).

Finally there was a bug in Grails 1.3.3 / Spock 0.5 which breaks mockDomain. This is reportedly fixed in 1.3.4 and the latest Spock code, but I haven't tried upgrading yet.

Here's how things really look...

@WithGMock
class MetaAttachmentsTagLibSpec extends TagLibSpec {

def g
def document

void setup() {
g = mock()
mock(tagLib).getG().returns(g).stub()
mockDomain Invoice

PluginManagerHolder.pluginManager = [hasGrailsPlugin: { String name -> true }] as GrailsPluginManager // Workaround for JIRA GRAILS-6482
}

def cleanup() {
PluginManagerHolder.pluginManager = null // Workaround for JIRA GRAILS-6482
}

def "Attachments icon has correct markup"() {
given:
Invoice invoice = new InvoiceBuilder().buildAndSave()
g.resource(instanceOf(Map)).returns '/foo.jpg'

when:
renderAttachmentsIcon([target: invoice])

then:
valueOf('img.@id') == "toggle-attachments-${invoice.id}"
valueOf('img.@src') == "/foo.jpg"
valueOf('script') == "\$('#toggle-attachments-${invoice.id}').bind('click', Books.attachments.toggle);"
}

def methodMissing(String name, args) {
String html
play {
html = super.methodMissing(name, args)
}
createDocument(html)
}

def createDocument(String html) {
String xml = """<!DOCTYPE html [<!ENTITY nbsp "_">]>\n<results>${html}</results>"""
document = new XmlSlurper().parseText(xml)
}
}

And the method under test...

def renderAttachmentsIcon = { Map attrs, def body ->
String targetId = attrs.target.id
String iconId = "toggle-attachments-${targetId}"
String imgSrc = g.resource(dir:'/images/skin', file:'paperclip.png')

MarkupBuilder builder = new MarkupBuilder(out)
builder.img(id: iconId, src: imgSrc)
builder.script(type:'text/javascript') {
mkp.yield "\$('#${iconId}').bind('click', Books.attachments.toggle);"
}
out << '' // flush for unit tests
}

Thursday, 2 September 2010

Internet Explorer and the 1 item remaining bug

We've started using Selenium 2 / WebDriver to soak test our application, and were caught once again by a bug in ie7 and ie8 that randomly causes the browser to hang, waiting for some resource to finish loading, even though the page is visible and works fine.

I hit the problem a year or so ago and solved it by hacking something in prototype.js, but can't remember exactly what. Thankfully there's now a more recent version of prototype (we're stuck on grails 1.1.1 which is bundled with prototype 1.6.0), so I upgraded to prototype 1.6.1 and problem solved :)