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.