Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Grails integration tests and transactions

I don't get why this integration test fails. I can get the test to pass by either removing the @Transactional(propagation = Propagation.REQUIRES_NEW) annotation above the service method, OR by setting transactional = false in the Integration Test

I realize that the integration test itself is running in a transaction, and that's why I've got the annotation on the service method.

class DbTests extends GrailsUnitTestCase {

boolean transactional = true
def customerService

void testTransactionsCommit() {
    def orderIds = [1, 2, 3]
    orderIds.each  { // lets make sure they all start out as Active
        def order = Order.get(it)
        order.isActive = true
        order.save(flush:true, validate:true, failOnError: true)
    }

    customerService.cancelOrders(orderIds)

    orderIds.each  {
        def order = Order.get(it).refresh()
        assertEquals false, order.isActive
    }
}

and my service method is defined:

class CustomerService {

boolean transactional = true
@Transactional(propagation = Propagation.REQUIRES_NEW)
def cancelOrders(def orderIds) {
    orderIds.each {
        Order order = Order.get(it)
        if(order.id == 5) //
            throw new RuntimeException('Simulating an exception here, panic!')
        order.isActive = false
        order.save(flush:true, validate:true, failOnError: true)
        println "Order.id = $order.id is ${order.isActive? 'ACTIVE' : 'CANCELLED'}"
    }
}}

The Order entity is a simple domain object and I'm on Grails 1.2.1, MySQL 5.x (dialect=org.hibernate.dialect.MySQL5InnoDBDialect)

I've seen this related post, but still no cigar :(

Grails Service Transactions

like image 524
Sunny Avatar asked Nov 10 '10 05:11

Sunny


1 Answers

Data changes that a nested, inner, transaction had committed, should be, indeed, instantly visible in the parent transaction.

And I really don't know why they are not in the transactional context of a GroovyTestCase. Others don't know, as well, and are using similar approaches to mine.

Consider the following test case. The test case, itself, is not transactional, but calls a transactional method. - This works as expected.

class TransactionalMethodTest extends GroovyTestCase {
    static transactional = false // test case is not transactional
    def customerService

    void testTransactionsCommit() {
        // start a new transaction, 
        // setting order 1 inactive
        setOrderInactive()
        assert ! Order.get(1).isActive
    }

    @Transactional(propagation = Propagation.REQUIRED)
    private void setOrderInactive() {
        // make sure that order 1 is active
        Order order = Order.get(1)
        order.isActive = true
        order.save(flush:true)

        assert Order.get(1).isActive

        // the following method acts in isolation level
        // Propagation.REQUIRES_NEW, which means,
        // a new, nested, transaction is started
        // >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
        customerService.cancelOrders([1])

        // changes from the nested transaction are
        // visible, instantly
        assert ! Order.get(1).isActive
        // <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
    }
}

Now consider the following, "normal", transactional, test case. Data changes from within the nested transaction are not visible in the parent transaction.

All I can say is, transactional test cases don't work with nested transactions, so use the non-transactional test case above.
If we don't understand the cause, we can, at least, know our options.

class TransactionalTestCaseTests extends GroovyTestCase {
    static transactional = true // default; Propagation.REQUIRED
    def customerService

    void testTransactionsCommit() {
        // make sure that order 1 is active
        Order order = Order.get(1)
        order.isActive = true
        order.save(flush:true)

        assert Order.get(1).isActive

        // the following method acts in isolation level
        // Propagation.REQUIRES_NEW, which means,
        // a new, nested, transaction is started
        // >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
        customerService.cancelOrders([1])

        // the changes from the inner transaction
        // are not yet visible
        assert Order.get(1).isActive
        // <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
    }

    @Override
    protected void tearDown() throws Exception {
        // the changes from the inner transaction
        // are still not visible
        assert Order.get(1).isActive

        super.tearDown();
    }
}

Not related to your primary question, but to your overall intent, here is a test case that checks whether the nested transaction is rolled back, properly:

class NestedTransactionRolledBackTests extends GroovyTestCase {
    static transactional = false // test case is not transactional
    def customerService

    void testTransactionsCommit() {
        // start a new transaction, 
        // setting order 1 active
        setOrderActive()
        assert Order.get(1).isActive
    }

    @Transactional(propagation = Propagation.REQUIRED)
    private void setOrderActive() {
        // make sure that order 1 is active
        Order order = Order.get(1)
        order.isActive = true
        order.save(flush:true)

        assert Order.get(1).isActive

        // the following method acts in isolation level
        // Propagation.REQUIRES_NEW, which means,
        // a new, nested, transaction is started.
        // This transaction will fail, and be rolled back.
        // >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
        shouldFail(NullPointerException) {
            customerService.cancelOrders([1, -999])
        }

        // changes from the nested transaction are
        // visible, instantly.
            // The changes have been rolled back
        assert Order.get(1).isActive
        // <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
    }
}

Finally, some more general sidenotes, it's not boolean transactional = true (which appears to work, though), but static transactional = true. Your integration tests should also extend GroovyTestCase, not its subclass GrailsUnitTestCase, as you don't need the latter's mocking capabilities. The isActive field should be named active as then, an isActive() getter will be automatically generated by naming convention.

like image 54
robbbert Avatar answered Oct 06 '22 22:10

robbbert