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 } }