I'm trying to get transactions working within a Grails service, but I'm not getting the results I'm expecting. Can someone tell me if I'm doing something wrong, if my assumptions are off?
My domain class:
class Account {
static constraints = {
balance(min: 0.00)
}
String companyName
BigDecimal balance = 0.00
Boolean active = true
String toString() {
return "${companyName} : ${balance}"
}
}
My service:
class AccountService {
static transactional = true
def transfer(Account source, Account destination, amount) throws RuntimeException {
if (source.active && destination.active) {
source.balance -= amount
if (!source.save(flush: true)) {
throw new RuntimeException("Could not save source account.")
} else {
destination.balance += amount
if (!destination.save(flush: true)) {
throw new RuntimeException("Could not save destination account.")
}
}
} else {
throw new RuntimeException("Both accounts must be active.")
}
}
def someMethod(Account account) throws RuntimeException {
account.balance = -10.00
println "validated: ${account.validate()}"
if(!account.validate()) {
throw new RuntimeException("Rollback!")
}
}
}
My unit test: import grails.test.*
class AccountServiceTests extends GrailsUnitTestCase {
def AccountService
protected void setUp() {
super.setUp()
mockDomain(Account)
AccountService = new AccountService()
}
protected void tearDown() {
super.tearDown()
}
void testTransactional() {
def account = new Account(companyName: "ACME Toy Company", balance: 2000.00, active: true)
def exception = null
try {
AccountService.someMethod(account)
} catch (RuntimeException e) {
exception = e
}
assert exception instanceof RuntimeException
println "exception thrown: ${exception.getMessage()}"
assertEquals 2000.00, account.balance
}
}
The result:
Testsuite: AccountServiceTests
Tests run: 1, Failures: 1, Errors: 0, Time elapsed: 1.068 sec
------------- Standard Output ---------------
--Output from testTransactional--
validated: false
exception thrown: Rollback!
------------- ---------------- ---------------
------------- Standard Error -----------------
--Output from testTransactional--
------------- ---------------- ---------------
Testcase: testTransactional took 1.066 sec
FAILED
expected:<2000.00> but was:<-10.00>
junit.framework.AssertionFailedError: expected:<2000.00> but was:<-10.00>
at AccountServiceTests.testTransactional(AccountServiceTests.groovy:89)
at _GrailsTest_groovy$_run_closure4.doCall(_GrailsTest_groovy:203)
at _GrailsTest_groovy$_run_closure4.call(_GrailsTest_groovy)
at _GrailsTest_groovy$_run_closure2.doCall(_GrailsTest_groovy:147)
at _GrailsTest_groovy$_run_closure1_closure19.doCall(_GrailsTest_groovy:113)
at _GrailsTest_groovy$_run_closure1.doCall(_GrailsTest_groovy:96)
at TestApp$_run_closure1.doCall(TestApp.groovy:66)
at gant.Gant$_dispatch_closure4.doCall(Gant.groovy:324)
at gant.Gant$_dispatch_closure6.doCall(Gant.groovy:334)
at gant.Gant$_dispatch_closure6.doCall(Gant.groovy)
at gant.Gant.withBuildListeners(Gant.groovy:344)
at gant.Gant.this$2$withBuildListeners(Gant.groovy)
at gant.Gant$th开发者_如何学Cis$2$withBuildListeners.callCurrent(Unknown Source)
at gant.Gant.dispatch(Gant.groovy:334)
at gant.Gant.this$2$dispatch(Gant.groovy)
at gant.Gant.invokeMethod(Gant.groovy)
at gant.Gant.processTargets(Gant.groovy:495)
at gant.Gant.processTargets(Gant.groovy:480)
My expectation:
When the account is given a negative balance, it shouldn't validate (which it doesn't), a RuntimeException should be thrown (which it is), and the account should rollback to it's previous state (balance: 2000), which is where it falls apart.
What am I missing here?
Unit tests are just Groovy or Java classes, so there's no Spring application context and hence no transaction support. You'd have to mock that for a unit test, but that wouldn't be testing transactionality, just the quality of the mocks. Convert this to an integration test and don't call new on the service, use dependency injection:
class AccountServiceTests extends GroovyTestCase {
def AccountService
void testTransactional() {
def account = new Account(companyName: "ACME Toy Company", balance: 2000.00,
active: true)
account.save()
assertFalse account.hasErrors()
String message = shouldFail(RuntimeException) {
AccountService.someMethod(account)
}
println "exception thrown: $message"
assertEquals 2000.00, account.balance
}
}
Note that the actual exception may be a wrapper exception with your thrown exception as its cause.
I tried the code but am seeing the same problem in the integration test. I used Grails 1.2
According to Jira GRAILS-3765 this is a known issue and still open. (I'm not sure why it only says "affects version 1.0.4" when 1.1.x has been out for a long time).
Based on these points, I think it is a bug in Grails. The Jira note has a workaround but I've not tried it. According to the issue, though, it will still work in when running the app; this can be confirmed manually.
What version of Grails are you running? v1.1.1 has a bug where transactional=true doesn't work properly.
精彩评论