Integration testing controllers with Grails 3

Grails testing framework makes it damn easy to write tests. Upto grails 2.x versions, grails supported writing both unit and integration tests for controllers, however suddenly with grails 3.x writing integration tests for controllers are no longer supported and grails recommends that we write the geb/functional tests instead.

But its better said then done. Most of grails applications which are developed with grails 2.x or earlier versions would have lots of controller integration tests and we cant just throw it and write the functional tests from scratch.

As we started migrating our grails 2.x application to grails 3.2.x, I was looking for a solution to get our controller integration tests running and passing with minimum changes. And finally after some hacks I have been able to get all our controller integrations tests pass with grails 3. (and we have dozones of them).

Below example shows how I did it.

How to write controller integration tests with Grails 3

I created a abstract base specification class which all our controller tests extends.

package me.nimavat

import grails.util.GrailsWebMockUtil
import groovy.transform.CompileStatic
import org.grails.plugins.testing.GrailsMockHttpServletRequest
import org.grails.plugins.testing.GrailsMockHttpServletResponse
import org.grails.web.servlet.mvc.GrailsWebRequest
import org.junit.Ignore
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.beans.factory.config.AutowireCapableBeanFactory
import org.springframework.mock.web.MockHttpServletRequest
import org.springframework.mock.web.MockHttpServletResponse
import org.springframework.web.context.WebApplicationContext
import org.springframework.web.context.request.RequestContextHolder
import spock.lang.Specification

@CompileStatic
abstract class BaseControllerIntegrationSpec extends Specification {

	@Autowired
	WebApplicationContext ctx

	void setup() {
		MockHttpServletRequest request = new GrailsMockHttpServletRequest(ctx.servletContext)
		MockHttpServletResponse response = new GrailsMockHttpServletResponse()
		GrailsWebMockUtil.bindMockWebRequest(ctx, request, response)
		currentRequestAttributes.setControllerName(controllerName)
	}

	@Ignore
	abstract String getControllerName()

	@Ignore
	protected GrailsWebRequest getCurrentRequestAttributes() {
		return (GrailsWebRequest) RequestContextHolder.currentRequestAttributes()
	}

	void cleanup() {
		RequestContextHolder.resetRequestAttributes()
	}

	@Ignore
	def autowire(Class clazz) {
		def bean = clazz.newInstance()
		ctx.autowireCapableBeanFactory.autowireBeanProperties(bean, AutowireCapableBeanFactory.AUTOWIRE_BY_NAME, false)
		bean
	}


	@Ignore
	def autowire(def bean) {
		ctx.autowireCapableBeanFactory.autowireBeanProperties(bean, AutowireCapableBeanFactory.AUTOWIRE_BY_NAME, false)
		bean
	}
}

If you have tried to call a controller action from an integration tests, you would know, that it throws the below error

java.lang.IllegalStateException: No thread-bound request found: Are you referring to request attributes outside of an actual web request, or processing a request outside of the originally receiving thread? If you are actually operating within a web request and still receive this message, your code is probably running outside of DispatcherServlet/DispatcherPortlet: In this case, use RequestContextListener or RequestContextFilter to expose the current request.
	at org.springframework.web.context.request.RequestContextHolder.currentRequestAttributes(RequestContextHolder.java:131)

The most important part of getting a controller action execute propery during integration test is to bind  a mock request and mock response with context.

		MockHttpServletRequest request = new   GrailsMockHttpServletRequest(ctx.servletContext)
		MockHttpServletResponse response = new GrailsMockHttpServletResponse()
		GrailsWebMockUtil.bindMockWebRequest(ctx, request, response)

 The next trick is to put the current controller name in request attribute. This is required if any code within controller or views depends on controllerName attribute. This is also needed to generate proper urls when a controller action redirects to another action within same controller, and does not explicitely specify the controller name.

currentRequestAttributes.setControllerName(controllerName)

Finally, the cleanup method clears the current request attributes. This will reset any changes made to the request or response during the test.

There is also a helper method to autowire a controller instance.

    @Ignore
	def autowire(def bean) {
		ctx.autowireCapableBeanFactory.autowireBeanProperties(bean, AutowireCapableBeanFactory.AUTOWIRE_BY_NAME, false)
		bean
	}

It can be used to create a new controller instance and autowire its dependencies as show below.

TestController controller = autowire(new TestController())

Controller integration test example.

Below is a complete example of an integration test for a sample controller.

class BookService {

	List list(int max) {
		return (1..max).collect({"Book-$it"})
	}
}

class BookController {
	BookService bookService

	def list() {
		int max = params.int("max")
		List books = bookService.list(max)
		return [books:books]
	}
}
import blog.me.nimavat.BookController
import grails.test.mixin.integration.Integration
import grails.transaction.Rollback


@Integration
@Rollback
class BookControllerSpec extends BaseControllerIntegrationSpec {
	String controllerName = "book"

	BookController controller

	void setup() {
		controller = autowire(BookController)
	}

	void "test list action"() {
		given:
		controller.params.max = 10

		when:
		Map result = controller.list()

		then:
		result.books != null
		result.books.size() == 10

	}
}

You can also autowire the controller in your test.

@Integration
@Rollback
class BookControllerSpec extends BaseControllerIntegrationSpec {
	String controllerName = "book"

	@Autowired
	BookController controller
	
}

Note: When you use autowired controller, if you change any property of your controller, of if mock any dependency of your controller it will affect all rest of the tests too.

This solution is probably not 100% perfect, and there would be edge cases where it can fail. However so far it has worked excellent and we have been able to migrate all our controller tests to grails 3.