Search code examples
grailsintegration-testingfunctional-testinggrails3

Grails 3 Integration Spec has Strange Transactional Behavior


I have the following test (which is probably more of a functional test than integration but...):

@Integration(applicationClass = Application)
@Rollback
class ConventionControllerIntegrationSpec extends Specification {

  RestBuilder rest = new RestBuilder()
  String url

  def setup() {
    url = "http://localhost:${serverPort}/api/admin/organizations/${Organization.first().id}/conventions"
  }

  def cleanup() {
  }

  void "test update convention"() {
    given:
    Convention convention = Convention.first()

    when:
    RestResponse response = rest.put("${url}/${convention.id}") {
      contentType "application/json"
      json {
        name = "New Name"
      }
    }

    then:
    response.status == HttpStatus.OK.value()
    Convention.findByName("New Name").id == convention.id
    Convention.findByName("New Name").name == "New Name"

  }
}

The data is being loaded via BootStrap (which admittadly might be the issue) but the problem is when I'm in the then block; it finds the Convention by the new name and the id matches, but when testing the name field, it is failing because it still has the old name.

While reading the documentation on testing I think the problem lies in what session the data gets created in. Since the @Rollback has a session that is separate from BootStrap, the data isn't really gelling. For example, if I load the data via the test's given block, then that data doesn't exist when my controller is called by the RestBuilder.

It is entirely possible that I shouldn't be doing this kind of test this way, so suggestions are appreciated.


Solution

  • This is definitely a functional test - you're making HTTP requests against your server, not making method calls and then making assertions about the effects of those calls.

    You can't get automatic rollbacks with functional tests because the calls are made in one thread and they're handled in another, whether the test runs in the same JVM as the server or not. The code in BootStrap runs once before all of the tests run and gets committed (either because you made the changes in a transaction or via autocommit), and then the 'client' test code runs in its own new Hibernate session and in a transaction that the testing infrastructure starts (and will roll back at the end of the test method), and the server-side code runs in its own Hibernate session (because of OSIV) and depending on whether your controller(s) and service(s) are transactional or not, may run in a different transaction, or may just autocommit.

    One thing that is probably not a factor here but should always be considered with Hibernate persistence tests is session caching. The Hibernate session is the first-level cache, and a dynamic finder like findByName will probably trigger a flush, which you want, but should be explicit about in tests. But it won't clear any cached elements and you run the risk of false positives with code like this because you might not actually be loading a new instance - Hibernate might be returning a cached instance. It definitely will when calling get(). I always add a flushAndClear() method to integration and functional base classes and I'd put a call to that after the put call in the when block to be sure everything is flushed from Hibernate to the database (not committed, just flushed) and clear the session to force real reloading. Just pick a random domain class and use withSession, e.g.

    protected void flushAndClear() {
       Foo.withSession { session ->
          session.flush()
          session.clear()
       }
    }
    

    Since the put happens in one thread/session/tx and the finders run in their own, this shouldn't have an effect but should be the pattern used in general.