Grails - Selenium RC Data Driving Te...

Data Driving Tests

One of the most important considerations when writing Selenium tests is how to set up the data that the tests require. If you are purely running your Selenium tests against localhost this is actually reasonably straightforward. Since the Selenium tests are running in the same JVM as the Grails application under test, the tests have direct access to the application's domain objects and can set up data very easily. For example:

void testSongsAppearInList() {
    Song.withTransaction {
        new Song(title: "Heads Will Roll", artist: "Yeah Yeah Yeahs").save(failOnError: true)
        new Song(title: "Twilight Galaxy", artist: "Metric").save(failOnError: true)
        new Song(title: "I'm Confused", artist: "Handsome Furs").save(failOnError: true)
    }
    selenium.open "$contextPath/song/list"
    assertEquals 3, selenium.getXpathCount("//table/tbody/tr")
}

Using a withTransaction block ensures you won't have problems with lazy-loading, failed data setup will get rolled back immediately and the data will be committed to the database before the browser hits the page. I also find that errors are reported better when data setup does fail.

Data can also be set up in the setUp method of the test case in exactly the same way.

Using the Build Test Data plugin

The Build Test Data plugin is very useful once your domain model goes beyond a trivial level of complexity and it can also be used directly in Selenium tests:

void testSongsAppearInList() {
    Song.withTransaction {
        3.times {
            Song.build() // assume we have some nice defaults in TestDataConfig.groovy
        }
    }
    selenium.open "$contextPath/song/list"
    assertEquals 3, selenium.getXpathCount("//table/tbody/tr")
}

An alternative to the build test data is the Fixtures plugin which can also be used directly from Selenium tests.

Tearing data down

The difficulty with data-driving Selenium tests like this is that unlike integration tests they are not run in a transactional context. Even if they were the problem would still arise as data modifications made using the browser (e.g. submitting an update form) would not be part of the same transaction. To keep your tests isolated from one another it is critical to use tearDown effectively in your tests to prevent data from one test 'bleeding over' into the next. With the examples above I could implement tearDown like this:

void tearDown() {
    super.tearDown()
    Song.withTransaction {
        Song.list()*.delete()
    }
}

The withTransaction block ensures that the deletes are flushed and committed to the database and that any problems deleting will get reported right away causing the test to fail.