Tuesday, December 2, 2008

Grails Integration Testing - Some Tips and Tricks

As stated in the Grails documentation "Integration tests differ from unit tests in that you have full access to the Grails environment within the test". This actually means that you have access to database, you can inject necessary services into the test class.

All integration tests have to extend GroovyTestCase class.

Database

By default, integration tests use in memory HSQLDB. You can change this by updating DataSource.groovy.

Next important feature to remember is that integration tests run within transaction that is rollbacked after the test. So if you want to have data within database after the testing, in the test specify boolean transactional = false.

Services

To use default injection of specified service classes within test all that is necessary is to specify service as a field in the test class. For the rest magic of the Grails will take care. E.g. example would be FeedService feedService

Domain Classes

Within integration test you can use domain classes same as you would in the regular code. The main difference is that in the most cases methods on the domain classes should be called with the flush:true parameter.

Knowing all these it seems that integration testing with grails is really easy. And it is the case but there are some additional features to take care (some of them are valid also for the production code).

Some Tips

Tip: Saving domain object can fail without any error

Yes this can happen and happens often when you forget about some validation rule. And when you are not aware that your object was not saved your test results cannot be correct.

So for example following code snippet will not save the object but also no exception will be thrown:

FeedSource feed = new FeedSource(feedLink:"http://abc.com", title:"abc")
feed.save(flush:true)
Article article = new Article(title:"afaf", articleLink:"http://abc.com", source:feed)
article = article.save(flush:true)

Reason is that there is 'description' field that is mandatory for the article. To ensure that you don't get such surprises I use following code:

private Object save(Object object) {
validateAndPrintErrors(object)
Object result = object.save(flush:true)
assertNotNull("Object not created: " + object, result)
return result
}

private void validateAndPrintErrors(Object object) {
if (!object.validate()) {
object.errors.allErrors.each {error ->
println error
}
fail("failed to save object ${object}")
}
}

To check that there are no errors in the domain object you need to call method validate(). If there are validation errors you can get them through errors property of the domain object.

Next thing to notice is that method save() of the domain object returns instance of created object if it is correctly saved. So you can check if returned object is different than null.

Code for saving and validating can also be implemented as extension to domain classes using some nice groovy magic. Further on, this methods also works only thanks to groovy magic.

So initial code snippet should be changed to following.

FeedSource feed = new FeedSource(feedLink:"http://abc.com", title:"abc")
feed = save(feed)

Article article = new Article(title:"afaf", articleLink:"http://abc.com", source:feed, description:"asbfasfd")
article = save(article)

In this case there will be no surprise that domain object was not created within database without any exception thrown.

Tip: In some cases you need to reload domain object from database

As base for the GORM magic is hibernate, and we know that hibernate uses cache, there are cases when you need to reload object from database to ensure that it is in correct state. So for example if the rule is that when feed is deleted source of the article should be set to null, following code may fail.

feedService.deleteFeed(feed)
// we are sure that this article exists
article = Article.findByTitle("afaf")
assertNull("Article source should be null but is ${article.source}", article.source)

Reason is that deleting feed will not update source field of the article in the hibernate cache and the last line will fail. To ensure that you really have latest object from the database you should call refresh method on the article.

feedService.deleteFeed(feed)
// we are sure that this article exists
article = Article.findByTitle("afaf")

article.refresh()

assertNull("Article source should be null but is ${article.source}", article.source)