Tuesday 20 August 2013

Grails pitfalls: Don't do flush:true when you actually want a transaction (and II)

Today somebody at work ask me about some statements I made in the previous entry "Grails pitfalls: Don't do flush:true when you actually want a transaction".

He thought that when doing

company.save(flush:true)

It didn't matter it was within or outside a transaction, the transaction will eventually commit the transaction event tough there were an exception on the way.

I didn't know that either until I worked in a project where I needed to return the number of persisted items just after persisting a new one but just before doing some critical calculation that could raise an exception and eventually rollback the entire transaction.

Think about it, if the domain wasn't persisted and "committed" (notice the quotes) I couldn't count the right number of persisted entities, but What if the subsequent calculation failed? I had already saved the domain class even tough it was incorrect to do so. It was a dead alley until I had a conversation with a senior engineer. More or less it was like the following:

Mario: Ciaran I can't save the instance and see it in the following line
Ciaran: Then flush:true the save statement
Mario: What? But save(flush:true) commits the statement.
Ciaran: Nop, it makes the instructions visible for the rest of the transaction
Mario: Ummm, I don't quite follow you but let's test it anyway.

That was the most difficult task, How to test that assertion?

Let's say we have the following service method:

    def saveAndFail(Company company) {
      company.user = springSecurityService.currentUser
      company.save(flush:true)
      throw new IllegalArgumentException("Ahhhh")
    }

I did the following test:
   package whatever

   import static org.junit.Assert.assertThat
   import static org.hamcrest.CoreMatchers.notNullValue
   import static org.hamcrest.CoreMatchers.is
     
   import grails.plugin.spock.IntegrationSpec
   import org.codehaus.groovy.grails.plugins.springsecurity.SpringSecurityUtils
         
  class CompanyServiceFlushIntegrationSpec extends IntegrationSpec{
       
    def companyService
         
    def "Test flush:true"(){ 
      setup: "Building a valid company instance"
        def user = validUser.save(failOnError:true,flush:true)
        def company = new Company(
          user: user,
          name: "name",
          companyCode : "companyCode"
        )
      when:"Saving a valid company instance"
          def savedCompany = SpringSecurityUtils.doWithAuth("username"){
              companyService.saveAndFail(company)
          }
      then: "The result should be the saved company"
        thrown(Exception)
        assertThat(Company.count(),is(0))
    }
        
    def getValidUser(){
      def validUser = new ApplicationUser(
        name: "name",
        username: "username",
        password: "password",
        locale: Locale.getDefault()
      ) 
    }       
        
 }


But it didn't work. I got:

Failure:  Test flush:true(whatever.CompanyServiceFlushIntegrationSpec)
|  java.lang.AssertionError: 
Expected: is <0>
     got: <1>

Let's analyze how the test worked. First, we should know that by default integration tests are transactional by default. That means that at the end of every test method if there is any transaction it will be rolled back....AT THE END. In our method we threw an exception "catched" by our thrown(Exception) statement. So there was no rollback there, so it makes sense that at line number 28 the assertion fails because the exception has not been propagated outside the test method, so that the count statement is aware of the persisted company because of the flush:true statement.

But I want to prove that the transaction actually does a rollback because of the exception we put on purpose. We can only do that establishing our own transaction scope. To do this is neccesary to declare the test as "not transactional", and then to wrap the service's invocation as it will happen at runtime.

   package whatever

   import static org.junit.Assert.assertThat
   import static org.hamcrest.CoreMatchers.notNullValue
   import static org.hamcrest.CoreMatchers.is
     
   import grails.plugin.spock.IntegrationSpec
   import org.codehaus.groovy.grails.plugins.springsecurity.SpringSecurityUtils
         
  class CompanyServiceFlushIntegrationSpec extends IntegrationSpec{
       
    static transactional = false

    def companyService
         
    def "Test flush:true"(){ 
      setup: "Building a valid company instance"
        def user = validUser.save(failOnError:true,flush:true)
        def company = new Company(
          user: user,
          name: "name",
          companyCode : "companyCode"
        )
      when:"Saving a valid company instance"
          Company.withTransaction{
            def savedCompany = SpringSecurityUtils.doWithAuth("username"){
              companyService.saveAndFail(company)
            }
          }
      then: "The result should be the saved company"
        thrown(Exception)
        assertThat(Company.count(),is(0))
    }
        
    def getValidUser(){
      def validUser = new ApplicationUser(
        name: "name",
        username: "username",
        password: "password",
        locale: Locale.getDefault()
      ) 
    }       
        
 }


And voila!

| Tests PASSED - view reports in...

I had no companies saved even though I used flush:true.

1 comment:

  1. Solo una pregunta. Si el test no es transaccional, la entidad Company se quedará después de pasar el test. ¿No sería mejor dejar el servicio como transaccional y hacer un Company.withNewTransaction? Si no habría que borrar el Company en el cleanup...

    ReplyDelete