nimavat.me

Java, Groovy, Grails, Spring, Vue, Ionic + Fun blog

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.