Cucumber & Grails: Transaction Rollback

… the short version is: “the cucumber grails plugin will not provide automatic transaction rollback”.

After adding automatic rollback code in the cucumber grails plugin (version 0.5) and trying to use it I had to learn, that the automatic part is flawed and that it does not work in all cases.

I will try to explain why it does not work and how the cucumber plugin will deal with it. If you happen to know more about how Grails/Spring transactions work, please correct me where I am wrong. If you have to add something please do! :-)

First some background how Grails handles database transactions by default.

runtime

If we use Grails default settings, it will wrap any service call into its own transaction. If we make two service calls in a single controller action, each one will have its own transaction. We will take a look at a simple example to demonstrate the convention. See also the Services chapter in the grails documentation.

You can change the defaults, but changing them will not improve anything. It will just make the situation more complicated and therefore it does only supports the statement that we will not get automatic transaction rollback for functional testing. :-)

Reusing the plugins Book example and running grails (run-app) we can easily check this behaviour.

package books

class Book {
    String author
    String title

    static constraints = {
        title unique: true
    }
}

The service looks like this:

package books

class BookService {
    Book add (boolean error, Map params) {
        def newBook = new Book (params)
        newBook.save (failOnError: true)
        if (error) {
            throw new RuntimeException ("rollback!")
        }
        newBook
    }
}

To easily get an error when we want to, I have added a parameter to the add() method that tells the function to error.

The following controller code uses the service with code you should not write in real code. It is written like this to demonstrate grails transaction handling. You should use a single service call to make the action run with a single transaction.

Now when we run the multipleTransactions() action in the following controller:

package books

class BookController {
    def bookService

    def multipleTransactions () {
        params.author = "A1"
        params.title = "T1"
        bookService.add (false, params)

        params.author = "A2"
        params.title = "T2"
        bookService.add (true, params)
    }
}

… and look at the database (you can use the dbconsole to look into the h2 database) we will see a single row:

ID VERSION AUTHOR TITLE
1 0 A10 T10

After reading Services this should not be a big surprise. Both add() calls to the service get their own transaction. Because of the true parameter in the second call, it throws and error an grails will automatically rollback the transaction. But only the transaction of the second call. The transaction of the first call is already committed.

integration test

In integration tests grails does automatically wrap each test in a transaction and triggers a rollback when the test is finished (which by the way is the code I called to get the rollback in the plugin).

It just does not work in all cases.

It fails when we exercise the application with http requests. I assume because grails session/request code will run (which is not run in an integration test when we simply new the controller) using its own transaction code that is ignoring the transaction wrapped around the test (guessing here, I don’t yet understand what grails does in detail).

It also fails if we start to run code using withTransaction().

package books

class BookService {
    Book add (Map params) {
        def newBook
        Book.withNewTransaction {
            newBook = new Book (params)
            newBook.save (failOnError: true)
        }
        newBook     
    }
}

.. with the controller:

package books

class BookController {
    def bookService

    def transaction () {
        params.author = "A1"
        params.title = "T1"
        bookService.add (params)
    }
}

… and when we run two cucumber scenarios calling BookController.transaction () the second call will fail because of the unique constraint on the title. The withTransaction() part will not be rollbacked.

conclusion

So where does this information place us regarding rollback of database transactions in context of functional testing with cucumber? Although it is a play example it shows that transaction handling can already get complex in such a simple example. Now consider multiple request, service annotations that change the default behaviour or “manual” withTransaction() calls. It is not as easy as it looks.

Maybe it would work if we were able to intercept the creation of any transaction created by grails, always return the same transaction and rollback it when the test is finished.

Appart from not finding any information how this would work (maybe I didn’t use the correct search words) it does not look like a good idea. Although it sounds like a solution in the first moment, it will make grails behave differently in the test enviroment and the production environment. How big the difference is, is hard to say. It depends on how much you change grails default behaviour.

Anyway, it will behave differently and I think transactions are important enough that we do not want this when doing functional testing. When we run end to end, the transactions should be identical to production.

My conclusion is, that the transaction handling is complicated enough that the plugin can not handle this for us. Therefore it will no longer run grails transaction code automatically. We will have to take care of cleaning up the database by ourself. Either by completely dropping and re-creating the database schema before each test or by running some statements that will restore the database to its original state (I will write another article showing an example for both ways).

The plugin will still provide some simple configuration to enable the integration test rollback support. You can use it to get the automatic rollback when you build functional integration tests, e.g. you do not exercise the app using http requests and you don’t use withTransaction().

IntelliJ IDEA & Spock: Groovy Pointless Arithmetic

Maybe this is just plain stupid but there is a simple workaround for the warning 0 * report.addError can be replaced by 0 in the following test (in IDEA):

def "does not report step error or failure when it succeeds" () {
  Result result = Mock (Result)
  result.error >> null

  when:
    uat.step (stepStub ())
    uat.result (result)

  then:
    0 * report.addError (_)
    0 * report.addFailure (_)
}

My solution so far was to add an @SuppressWarnings(["GroovyPointlessArithmetic"]) to the test class. But there is a simpler solution without disabling the warning. I can simply write

def "does not report step error or failure when it succeeds" () {
  Result result = Mock (Result)
  result.error >> null

  when:
    uat.step (stepStub ())
    uat.result (result)

  then:
    (0) * report.addError (_)
    (0) * report.addFailure (_)
}

to remove the warning. See the difference? I have replaced the 0 with (0) and the warning goes away. :-)