I found that using Groovy's MockFor is just as convenient and does not contain this shortcoming.
Concept Overview
Often a developer would like to use mocks to isolate code and verify that a unit of code is operating correctly. The rationale is: "Given that collaborators are behaving in a specific way, the code under test behaves as expected." In other words, if collaborator returns A, then the code under test will perform B. If the collaborator returns C, then the code under test will perform D.
Many Java mock frameworks provide this ability, including EasyMock and JMock, to name a few.
mockFor Shortcoming
On a grails project, I wanted a mock to return a certain value. This did not work. Let me provide an example to illustrate the problem.
Let's say I have a piece of code named AccountService that checks an account balance. When the account balance is below $25, its raises an alert to the screen and returns false. If the balance is above $25 it proceeds for processing. AccountService depends on BalanceService. BalanceService checks the balance of the Account.
package com.solutionsfit
class AccountService {
def balanceService
def process() {
if (balanceService.checkBalance() < 25) {
raiseAlert()
return false
} else {
// process account normally.
}
return true
}
def raiseAlert() {
println "Balance is below \$25!"
}
}
package com.solutionsfit
class BalanceService {
def checkBalance() {
// checks the balance of the account.
}
}
I want to mock the balance service with mockFor in grails unit testing. I want achieve two things: 1) verify checkBalance was called once 2) manipulate the return value of checkBalance so I can test accountService.process(). Here is the test case using mockFor. We get this error:
groovy.lang.GroovyRuntimeException: Cannot compare com.solutionsfit.AccountServiceTests$_testProcess_accountIsTooLow_closure1 with value 'com.solutionsfit.AccountServiceTests$_testProcess_accountIsTooLow_closure1@ff9053' and java.lang.Integer with value '25'
at com.solutionsfit.AccountService.process(AccountService.groovy:8):8)
package com.solutionsfit
import grails.test.*
class AccountServiceTests extends GrailsUnitTestCase {
AccountService accountService = new AccountService()
protected void setUp() {
super.setUp()
}
protected void tearDown() {
super.tearDown()
}
void testProcess_accountIsTooLow() {
// create the mock control and set as collaborator in accountService
def mockBalanceServiceControl= mockFor(BalanceService)
accountService.balanceService = mockBalanceServiceControl.createMock()
// set expectation and give a mock return value
mockBalanceServiceControl.demand.checkBalance(1..1) { -> return 24 }
def result = accountService.process()
assertFalse("process should return false due to low balance", result)
mockBalanceServiceControl.verify()
}
}
Using MockForI can implement a correct test by using Groovy's native MockFor implementation. The syntax is slightly different but the test runs as expected. Here is an example of the test class.
package com.solutionsfit
import grails.test.*
import groovy.mock.interceptor.MockFor
class AccountServiceTests extends GrailsUnitTestCase {
AccountService accountService = new AccountService()
protected void setUp() {
super.setUp()
}
protected void tearDown() {
super.tearDown()
}
void testProcess_accountIsTooLow() {
// create the mock control and set as collaborator in accountService
def mockContext = new MockFor(BalanceService)
// set expectation and give a mock return value
mockContext.demand.checkBalance(1..1) { -> return 24 }
def mockBalanceService = mockContext.proxyInstance()
accountService.balanceService = mockBalanceService
def result = accountService.process()
assertFalse("process should return false due to low balance", result)
mockContext.verify(mockBalanceService)
}
void testProcess_accountIsAboveThreshold() {
// create the mock control and set as collaborator in accountService
def mockContext = new MockFor(BalanceService)
// set expectation and give a mock return value
mockContext.demand.checkBalance(1..1) { -> return 50 }
def mockBalanceService = mockContext.proxyInstance()
accountService.balanceService = mockBalanceService
def result = accountService.process()
assertTrue("process should return true as balance is above threshold", result)
mockContext.verify(mockBalanceService)
}
}
ConclusionGrails mockFor certainly has a defect in it. I also tried this code in grails 1.2.5 and the same issue occurs. I have not tried it in future versions. However, MockFor is a good alternative and provides many other features. Alternatively, you can use Expandos, ExpandoMetaClass and Map Coercion to mock out collaborators. These options are low ceremony and don't provide out of the box validations. The mocking implementation you choose depends on the context of testing and the extent of validations you want to enforce.
Special thanks to Arvind Dhiman for his insite on this issue
Assir
ReplyDeleteIn your demand you defined your closure incorrectally.
You entered:
mockBalanceServiceControl.demand.checkBalance(1..1) { return 24 }
You should have entered
mockBalanceServiceControl.demand.checkBalance(1..1) { -> return 24 }
Since the closure takes zero parameters, you have to define it as { -> ... } not { }
Best Regards,
Paul WOods
Yes, Paul. Thanks you are correct. I have run into this issue before with mocking closures, and it had caused problems in the past. I am correcting the code in the blog. Thanks!
ReplyDeleteIs there any way to get MockFor to work when mocking a service or interface defined in Java? (e.g. with non dynamic return type)
ReplyDeleteYes, you can use this in Java. I haven't used it for testing Java classes, but the documentation says it can. You must use instance style with proxy. See "Instance-style MockFor and StubFor" section, last paragraph here:
ReplyDeletehttp://groovy.codehaus.org/Groovy+Mocks