Wednesday, April 29, 2009

Mocking Transactions for unit testing a Grails Service

Use Case:
As a good developer
I should be able to unit test services that programmatically use transactions
So that I can minimize defects  and have high unit test coverage
The Example Specs:
A persistent Foo object has a bar field which is of type String. Write a service with a doSomethingTransactional method that accepts a Foo object as a parameter. The method should change the value of bar to 'did something transactional' if the value of bar is NOT 'oops'
The Domain:
package com.henyo.model

class Foo {

    String bar

    static constraints = {
    }
}
The Unit Test:
package com.henyo.service

import grails.test.*
import com.henyo.model.*
import com.henyo.service.*
import com.henyo.*

class FooServiceTests extends BaseUnitTestCase{

    void testShouldCommit(){
        def foo = new Foo(bar:'not oops')
        mockForTransaction(Foo)
        def service = new FooService()
        service.doSomethingTransactional(foo)
    }

    void testShouldRollBack(){
        def foo = new Foo(bar:'oops')
        mockForTransaction(Foo,true)
        def service = new FooService()
        service.doSomethingTransactional(foo)
    }
}
The unit test above takes into account 2 scenarios. One where we expect that the transaction will be committed and the other one will signal a rollback. The mockForTransaction actually returns a TransactionStatus object which can be used to check if the transaction is indeed committed or if it is for roll back. There is no need though to test for this as BaseUnitTestCase handles this for you automatically.
The Service:
package com.henyo.service

import com.henyo.model.*

class FooService {

    boolean transactional = false

    def doSomethingTransactional = { foo ->
        Foo.withTransaction{ status ->
            if(foo.bar == 'oops'){
                status.setRollbackOnly()
            }
        }
    }
    
}
My BaseUnitTestCase:
package com.henyo

import grails.test.GrailsUnitTestCase
import org.springframework.transaction.TransactionStatus

class BaseUnitTestCase extends GrailsUnitTestCase {

    def statusControls

    protected void setUp() {
        super.setUp()
        statusControls = []
    }

    protected void tearDown(){
        statusControls.each{
            it.verify()
        }
        statusControls.clear()
        super.tearDown()
    }

    def mockForTransaction(Class clazz,boolean expectRollback = false){
        registerMetaClass(clazz)
        def statusControl = mockFor(TransactionStatus)
        statusControls << statusControl
        if(expectRollback)
            statusControl.demand.setRollbackOnly(1..1) { println 'setRollbackOnly called'}
        def status = statusControl.createMock()
        clazz.metaClass.'static'.withTransaction = {
            Closure callable ->  callable.call(status)
        }
        return statusControl
    }
}
Did it work for you? You are welcome to post your comments/questions or better yet, link to this post, blog about it and tell all your friends who might find this post useful.

7 comments:

  1. Very helpful. Thanks!
    It just works!

    ReplyDelete
  2. You are welcome. Am glad to be of some help. Suggestions to improve on the process or the code is also welcome

    ReplyDelete
  3. i've got error message when i run grails test-app
    No signature of method: static Foo.withTransaction() is applicable for argument types: (FooService$_closure1_closure2) values: [FooService$_closure1_closure2@1a5d6d6]

    ReplyDelete
  4. Which version of grails are you using?

    ReplyDelete
  5. thank you for your reply :)
    my grails version is grails 1.2.1

    ReplyDelete
  6. @B_CORPSE
    I updated the sample code. It now works for grails 1.2.1.

    Let me know if you want the skeletion grails project. I could zip it up and send it to you

    ReplyDelete
  7. truely thank you,can u send skeleton project to this email bcorpse(AT)gmail.com.

    ReplyDelete