Analytics

Thursday, December 30, 2010

mockFor and MockFor in Grails

I recently came across a small shortcoming in Grails' mockFor feature. I wasn't able to return a value from a service that was mocked. I get an error where the return value is always a closure, not the value I intended. (Note this occurred in grails v1.1).

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 MockFor

I 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)
    }
}
Conclusion

Grails 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

9 comments:

  1. Assir

    In 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

    ReplyDelete
  2. 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!

    ReplyDelete
  3. Is there any way to get MockFor to work when mocking a service or interface defined in Java? (e.g. with non dynamic return type)

    ReplyDelete
  4. Yes, 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:

    http://groovy.codehaus.org/Groovy+Mocks

    ReplyDelete
  5. Reading white on black, ouh la la.. my poor eyes.

    ReplyDelete
  6. Thanks for your simple example of mockFor Vs. MockFor. For me the real problem with the Grails mockFor is that it is not available for integration testing but you get no error message about that if you accidentally use it (beside the fact that it worked in older Grails versions).

    When I was doing some testing before I think I ran into this too, but I was able to solve it by changing the order of the createMock and demand statements from:


    accountService.balanceService = mockBalanceServiceControl.createMock()
    mockBalanceServiceControl.demand.checkBalance(1..1) { -> return 24 }


    to:


    mockBalanceServiceControl.demand.checkBalance(1..1) { -> return 24 }
    accountService.balanceService = mockBalanceServiceControl.createMock()


    I notice in your MockFor code you actually had this change.

    Dan Ketcham

    ReplyDelete
  7. Thanks, i appreciate the feedback.

    ReplyDelete
  8. very informative article.And very well explained about different protocols.keep posting like good content posts.For more details please visit our website.
    Oracle Fusion Cloud HCM Online Training

    ReplyDelete