Tuesday, 22 February 2011

Remote Controlled Geb Functional Tests

I've just started on a new project where Geb and Spock are our main functional testing tools. They're nice tools to work with but sometimes it's hard to write tests quickly when you can't see what's going on. I want to be able to step through tests like in Selenium IDE but without all the shortcomings of using fixture controllers.

One way to develop functional tests under Grails is to keep the app running while you launch tests from another jvm and use -baseUrl to target the remote jvm. You can do this from the command line or use the Functional Test Development plugin. Another way is to keep the app running and run the tests from within an IDE like IntelliJ; a bonus is that you can run individual tests/features which is great if you have a fairly large spec.

Whichever way you choose, you're going to need to setup data on the target jvm. On my last project we used fixture controllers that we would call from our functional tests but these grew into thousands of lines because re-use after a while became difficult. A better way to do it is to bundle the test data with the actual test which is what the Remote Control plugin allows us to do - the plugin documentation has a better explanation of what I mean.

With all that said, what I want to be able to do is:
  • Run my app up
  • Create my data in my setup block as though I was in an integration test.
  • Right click on the test from intellij and hit run (or use debug and step through line by line)
Take a look at the plugin docs, it's pretty straight forward but we had to override getBaseUrl and create our own BuildSettingsHolder because remote control doesn't like running outside of a grails command line. So our parent Spec looked like:
public ParentSpec extends GebSpec {
String getBaseUrl() {
"http://localhost:8080/monkeytails/"
}

def setupSpec() {

if (BuildSettingsHolder.getSettings() == null) {
BuildSettings bs = new BuildSettings()
bs.metaClass.getFunctionalTestBaseUrl = { getBaseUrl() }

BuildSettingsHolder.setSettings(bs)
}
}

def cleanupSpec() {
resetDatabase()
}

void resetDatabase() {
def remote = new RemoteControl()
remote {
User.list().each{ it.delete(flush:true) }
// Other teardown stuff goes here
// ...
return true
}
}
}
and then your tests:
    def "Test something"() {

setup:
def remote = new RemoteControl()
remote {
User user = new User()
user.save(flush: true)
return true //have to return something serializable
}
.
.
}
It's handy to have firebug in the firefox that webdriver opens so use the profile manager to create a profile called test and then change your IntelliJ JUnit Run configuration VM Parameters to include '-Dgeb.driver=firefox -Dwebdriver.firefox.profile=test'

Now you when you right click and run your test, IntelliJ will first compile everything and hopefully run your test. I say hopefully because I have a bunch of inline plugins so I have to exclude them from the compile path and hope for the best. Setting breakpoints and choosing debug lets you step through your test and let you can use firebug within the browser.

There's a few things to be aware of. Groovy's property setters don't seem to work so you can't do stuff like user.username, instead you have to use the setter or if there isn't one you can use setProperty(). Spread operators don't work too well either, so you can't do User.list()*.delete().

Update: forgot to mention that you should run the app in test mode or Remote Control will not set up its listener unlesss you set remoteControl.enabled to true in your app config

3 comments:

Kris said...
This comment has been removed by the author.
Kris said...

All good, except the remote control plugin falls down when you start using DSL's inside your remote block. We have been using the fixtures plugin to build up test data. At the last time of testing this didn't play well, which is a shame. Rob and I managed to make a remote version of the fixtures plugin but this meant putting the fixture block in a string to stop it evaluating. Not nice. We still haven't reached a conclusion on which is the lesser of evils. Long setup blocks newing up domain objects with the remote plugin or our remote fixture plugin. Sigh. ;-)

Kris said...

Another thing I just noticed, we only over rode the getBaseUrl() method with the following

String getBaseUrl() {
super.getBaseUrl() ?:
ConfigurationHolder.config?.grails?.serverURL ?: "http://localhost:8080"
}

That was enough to let us run tests in the IDE and at the command line.