Friday, May 6, 2016

Mocking DSL From a Plugin

Sometimes, you want to test some Grails logic that uses one of the nice DSLs (Domain Specific Language) from a plugin.  For example, you are using the Mail Service Plugin and want to test the code that calls it:

mailService.sendMail {
  to email
  subject title
  text body
}

This is not simple with normal mocking tools.  What you need is a simple Mock mechanism that records the method calls made via the DSL.  The DSLMock class (shown at the end) can handle this.  The test will look something like this:

@Testvoid testAlertLogging() {
  def mailDSLMock = DSLMock.mock(MailService, 'sendMail', [])
  service.mailService = new MailService()

  service.triggerAlert('dummy@abc.xyz')
  // check email was sent to the right user with the right subject/body
  assert mailDSLMock.result.contains([name: 'to', args: ['dummy@abc.xyz']])
  def subjectMap = mailDSLMock.result.find() { it.name == 'subject' }
  assert subjectMap.args[0] == 'Default Subject'

  def bodyMap = mailDSLMock.result.find() { it.name == 'text' }
  assert body.bodyMap.args[0].contains('alert')
}


The DSLMock will simply record the calls to the DSL and your test code must verify the correct calls were made.

The DSLMock class is:

class DSLMock {
  /**   
  * Creates a mock for the given clazz and method.   
  * @param clazz The class to add the mock to.   
  * @param method The method that starts the DSL.   
  * @param result The expected result of the DSL.   
  * @return The mock.   */
  static DSLMock mock(clazz, method, result) {
    def tester = new DSLMock()
    clazz.metaClass.static."${method}" = { Closure dsl ->
      dsl.delegate = tester
      dsl.resolveStrategy = Closure.DELEGATE_FIRST      dsl()
      return result
    }
    return tester
  }

  /**   * The list of method calls made to the mocked DSL and their arguments.   */

  def result = []

  /**   
  * Used to record the method calls made to the mock.   
  * @param name The method name.    
  * @param args The arguments.   
  * @return   */

  def methodMissing(String name, args) {
    result << [name: name, args: args.toList()]
  }
}

(Sorry for the formatting of the code.  I am still learning how to use the blog editor).

Grails 3 Testing Hints/Tips

Grails 3.x changed a lot of things. Most for the good, but this introduced a number of changes in our test code.  Some of the misc hints/tips for testing are:
  • Don't ever put @Integration in a test super-class.  Only use this in the concrete test classes.
  • IDEA needs the tests to be marked with @Test to run in the IDEA/JUnit world.  Gradle can still run them, but the IDEA JUnit system misses them.
  • Don't use @Rollback together with .withTransaction{} in most cases.  The .withTransaction{} gets rolled back too.
  • Unit Tests need to use the save(flush:true)syntax now.
  • For GEB testing of GUIs, never use @Transaction.  Instead use .withTransaction{} to create records.  The test code will not commit until it exits the method, so the server will never see the data while the test method is running.
  • To test a controller in an integration test, you will need to send an HTTP request to the running server. I use HTTPClient for this.  A login is needed to do this (similar to GEB, but at the pure HTTP level, no GUI).
  • application.groovy is needed for custom GORM data types.  The application.yml does not support the grails.gorm.default.mapping.user-type syntax needed for a custom user type.
(The last one is not test related, but I did not want to create a new blog post for it :).

Some of these could be bugs that might need fixes, but I am just not sure they are worth the time to fix them.  The work-arounds seem acceptable.