Show Navigation

Grails Controller Testing

In this guide, we explore Controller testing in Grails.

Authors: Nirav Assar, Sergio del Amo

Grails Version: 5.0.1

1 Grails Training

Grails Training - Developed and delivered by the folks who created and actively maintain the Grails framework!.

2 Getting Started

In this guide, you explore Controller Testing in Grails. We are going to focus on some of the most common use cases:

Unit Tests

  • Validate allowed methods.

  • Validate model, views, status and redirect

  • Use a Stub when you work with a service

  • Validate HTTP parameters bind to a command object

  • Validate a JSON payload is bound to a command object

Functional Tests

  • JSON response, status code

2.1 What you will need

To complete this guide, you will need the following:

  • Some time on your hands

  • A decent text editor or IDE

  • JDK 1.8 or greater installed with JAVA_HOME configured appropriately

2.2 How to complete the guide

To get started do the following:

or

The Grails guides repositories contain two folders:

  • initial Initial project. Often a simple Grails app with some additional code to give you a head-start.

  • complete A completed example. It is the result of working through the steps presented by the guide and applying those changes to the initial folder.

To complete the guide, go to the initial folder

  • cd into grails-guides/grails-controller-testing/initial

and follow the instructions in the next sections.

You can go right to the completed example if you cd into grails-guides/grails-controller-testing/complete

3 Writing the Application

We are going to write a simple CRUD application involving one domain class Student.

We will have one Controller and one Service class. We will be able to explore Controller testing approaches within this context.

3.1 Domain Class and Scaffold

We will create a Domain class Student.

A domain class fulfills the M in the Model View Controller (MVC) pattern and represents a persistent entity that is mapped onto an underlying database table. In Grails a domain is a class that lives in the grails-app/domain directory.

> grails create-domain-class Student

Add domain properties (name and grade) to the created domain class:

grails-app/domain/demo/Student.groovy
package demo

import groovy.transform.CompileStatic

@CompileStatic
class Student {
    String name
    BigDecimal grade

    String toString() {
        name
    }
}

If you generated the domain class with the create-domain-class command, a Spock Specification for the domain class is created.

Delete this file:

$ src/test/groovy/demo/StudentSpec.groovy

Learn more about testing domain classes in the How to test Domain Class constraints? Guide.

3.2 Static Scaffolding

Generate a Controller and Views for this domain class with the help of Grails Static scaffolding capabilities.

Learn more about scaffolding in the Grails documentation.
> grails generate-all demo.Student

The previous command generates a sample controller and GSP views. It generates a Spock Specification for the generated controller.

.grails-app/controllers/demo/StudentController.groovy
.grails-app/views/student/create.gsp
.grails-app/views/student/edit.gsp
.grails-app/views/student/index.gsp
.grails-app/views/student/show.gsp
.src/test/groovy/demo/StudentControllerSpec.groovy

Those artifacts provide CRUD functionality for the Student domain class.

We have edited the generated controller and moved the code dealing with Transactional behavior into a service. You can check the complete solution in the complete folder.

The Grails team discourages the embedding of core application logic inside controllers, as it does not promote reuse and a clean separation of concerns. A controller’s responsibility should be to handle a request and create or prepare a response. A controller can generate the response directly or delegate to a view.

3.3 Unit Test - Index Method

The index() action method returns a studentList and studentCount as a model back to the view.

Next code sample, shows the index method of the StudentController.

grails-app/controllers/demo/StudentController.groovy
package demo


import static org.springframework.http.HttpStatus.CREATED
import static org.springframework.http.HttpStatus.OK
import static org.springframework.http.HttpStatus.NOT_FOUND
import static org.springframework.http.HttpStatus.NO_CONTENT
import org.springframework.context.MessageSource
import groovy.transform.CompileDynamic
import groovy.transform.CompileStatic


@CompileStatic
class StudentController {

    static allowedMethods = [save: 'POST',
                             update: 'PUT',
                             delete: 'DELETE',]

    StudentService studentService

    MessageSource messageSource

    def index(Integer max) {
        params.max = Math.min(max ?: 10, 100)
        List<Student> studentList = studentService.list(params)
        respond studentList, model: [studentCount: studentService.count()]
    }

}

The previous action uses a service (the collaborator) to get some objects. Then, it returns those objects as the model. We are going verify that the objects returned by the service are indeed used as the model. To see this we will use a Stub to help us.

When should I use a Mock, and when should I use a Stub?

From the book Spock Up & Running

If the test is concerned with proving that the test subject interacts with a collaborator in a particular way, use a mock. If the fact that a collaborator behaves in a certain way exposes a particular behavior in the test subject the outcome of that behavior is what you are testing, use a stub

src/test/groovy/demo/StudentControllerSpec.groovy
package demo

import grails.testing.web.controllers.ControllerUnitTest
import spock.lang.Specification

class StudentControllerSpec extends Specification implements ControllerUnitTest<StudentController> {
    def 'Test the index action returns the correct model'() {
        given:
        List<Student> sampleStudents = [new Student(name: 'Nirav', grade: 100),
                                        new Student(name: 'Jeff', grade: 95),
                                        new Student(name: 'Sergio', grade: 90),]
        controller.studentService = Stub(StudentService) {
            list(_) >> sampleStudents
            count() >> sampleStudents.size()
        }

        when: 'The index action is executed'
        controller.index()

        then: 'The model is correct'
        model.studentList (1)
        model.studentList.size() == sampleStudents.size()
        model.studentList.find { it.name == 'Nirav' && it.grade == 100 }
        model.studentList.find { it.name == 'Jeff' && it.grade == 95 }
        model.studentList.find { it.name == 'Sergio' && it.grade == 90 }
        model.studentCount == sampleStudents.size()
    }
}
1 We can verify the model contains the information we expect

3.4 Unit Test - Save Method

The save() action method use a command object as a parameter.

Check the guide Using Command Objects to handle form data to learn more about command objects.

grails-app/controllers/demo/StudentSaveCommand.groovy
package demo

import grails.compiler.GrailsCompileStatic
import grails.validation.Validateable

@GrailsCompileStatic
class StudentSaveCommand implements Validateable {
    String name
    BigDecimal grade

    static constraints = {
        name nullable: false
        grade nullable: false, min: 0.0
    }
}
grails-app/controllers/demo/StudentController.groovy
package demo


import static org.springframework.http.HttpStatus.CREATED
import static org.springframework.http.HttpStatus.OK
import static org.springframework.http.HttpStatus.NOT_FOUND
import static org.springframework.http.HttpStatus.NO_CONTENT
import org.springframework.context.MessageSource
import groovy.transform.CompileDynamic
import groovy.transform.CompileStatic


@CompileStatic
class StudentController {

    static allowedMethods = [save: 'POST',
                             update: 'PUT',
                             delete: 'DELETE',]

    StudentService studentService

    MessageSource messageSource

    @CompileDynamic
    def save(StudentSaveCommand cmd) {
        if (cmd.hasErrors()) { (1)
            respond cmd.errors, [model: [student: cmd], view: 'create']
            return
        }

        Student student = studentService.save(cmd) (2)
        if (student.hasErrors()) { (3)
            respond student.errors, view:'create'
            return
        }

        request.withFormat {  (4)
            form multipartForm {  (5)
                String msg = messageSource.getMessage('student.label', [] as Object[], 'Student', request.locale)
                flash.message = messageSource.getMessage('default.created.message', [msg, student.id] as Object[], 'Student created', request.locale)
                redirect(action: 'show', id: student.id)
            }
            '*' { respond student, [status: CREATED] }
        }
    }

}
1 Grails binds the parameters in the command object and calls validate() before the controller’s save action starts.
2 If the command object is valid, it tries to save the new student with the help of a service (a collaborator).
3 Inspect the collaborator response. The action result depends on the collaborator’s response.
4 If everything went well, it would return a different answer depending on the Content Type.
5 For example, for "application/x-www-form-urlencoded Content Type, it redirect to the show action to display the Student.

We could unit test it with the next feature methods:

src/test/groovy/demo/StudentControllerSpec.groovy
package demo


import grails.testing.web.controllers.ControllerUnitTest
import spock.lang.Specification


class StudentControllerSpec extends Specification implements ControllerUnitTest<StudentController> {

    def 'If you save without supplying name and grade(both required) you remain in the create form'() {

        when:
        request.contentType = FORM_CONTENT_TYPE (1)
        request.method = 'POST' (2)
        controller.save()

        then:
        model.student
        view == 'create' (3)
    }

}
1 Thanks to the @TestFor annotation, we change the request content type. The action under test outputs a different status code depending on the ContentType see request.withFormat closure.
2 Thanks to the @TestFor annotation, we change the request method
3 We can verify the view used.
src/test/groovy/demo/StudentControllerSpec.groovy
package demo


import grails.testing.web.controllers.ControllerUnitTest
import spock.lang.Specification


class StudentControllerSpec extends Specification implements ControllerUnitTest<StudentController> {

    def 'if the users supplies both name and grade, save is successful '() {
        given:
        String name = 'Nirav'
        BigDecimal grade = 100
        Long id = 1L
        controller.studentService = Stub(StudentService) {
            save(_, _) >> new Student(name: name, grade: grade, id: id)
            read(_) >> new Student(name: name, grade: grade, id: id)
        }
        when:
        request.method = 'POST'
        request.contentType = FORM_CONTENT_TYPE
        params['name'] =  name (1)
        params['grade'] = grade
        controller.save() (2)

        then: 'a message indicating that the user has been saved is placed'
        flash.message (3)

        and: 'the user is redirected to show the student'
        response.redirectedUrl.startsWith('/student/show') (4)

        and: 'a found response code is used'
        response.status == 302 (5)
    }

}
1 Supply the parameters as you would normally do.
2 Don’t supply parameters to the action method. Call the action method without parameters. Grails does the binding. It simulates more closely what happens in real code.
3 You can check that a flash message has been populated
4 Verify the redirected url is what we expected.
5 You can verify the expected status code is present.
src/test/groovy/demo/StudentControllerSpec.groovy
package demo


import grails.testing.web.controllers.ControllerUnitTest
import spock.lang.Specification


class StudentControllerSpec extends Specification implements ControllerUnitTest<StudentController> {

    void 'JSON payload is bound to the command object. If the student is saved, a 201 is returned'() {
        given:
        String name = 'Nirav'
        BigDecimal grade = 100
        Long id = 1L
        controller.studentService = Stub(StudentService) {
            save(_, _) >> new Student(name: name, grade: grade, id: id)
        }

        when: 'json request is sent with domain conversion'
        request.method = 'POST'
        request.json = '{"name":"' + name + '","grade":' + grade + '}' (1)
        controller.save()

        then: 'CREATED status code is set'
        response.status == 201 (2)
    }

}
1 Grails supports data binding of JSON requests to command objects.
2 You can verify the expected status code is present.

3.5 Test Allowed Methods

We have restricted the save action to only POST requests with the help of the property allowedMethods.

allowedMethods: Limits access to controller actions based on the HTTP request method, sending a 405 (Method Not Allowed) error code when an incorrect HTTP method is used.

grails-app/controllers/demo/StudentController.groovy
    static allowedMethods = [save: 'POST',
                             update: 'PUT',
                             delete: 'DELETE',]

We can verify it with a unit test:

src/test/groovy/demo/StudentControllerAllowedMethodsSpec.groovy
package demo

import static javax.servlet.http.HttpServletResponse.SC_METHOD_NOT_ALLOWED
import static javax.servlet.http.HttpServletResponse.SC_OK
import grails.testing.web.controllers.ControllerUnitTest
import spock.lang.Specification
import spock.lang.Unroll

@SuppressWarnings(['JUnitPublicNonTestMethod', 'JUnitPublicProperty'])
class StudentControllerAllowedMethodsSpec extends Specification implements ControllerUnitTest<StudentController> {

    @Unroll
    def "StudentController.save does not accept #method requests"(String method) {
        when:
        request.method = method
        controller.save()

        then:
        response.status == SC_METHOD_NOT_ALLOWED

        where:
        method << ['PATCH', 'DELETE', 'GET', 'PUT']
    }

    def "StudentController.save accepts POST requests"() {
        when:
        request.method = 'POST'
        controller.save()

        then:
        response.status == SC_OK
    }
}

3.6 Functional Test

We are going to use a functional test to verify the index method returns a JSON payload with a list of Students.

We can use HttpClient from the Micronaut HTTP library. Import the dependency into the build.gradle.

build.gradle
dependencies {
...
    testImplementation "io.micronaut:micronaut-http-client"
}

We can invoke the controller actions by using the appropriate URL in the HttpClient API. We will get back a HttpResponse object and with that object, we can validate the returned items of the controller.

The next feature method invokes the index() action and return a JSON list of students.

src/integration-test/groovy/demo/StudentControllerIntSpec.groovy
package demo

import grails.testing.mixin.integration.Integration
import grails.testing.spock.OnceBefore
import io.micronaut.core.type.Argument
import io.micronaut.http.HttpRequest
import io.micronaut.http.HttpResponse
import io.micronaut.http.HttpStatus
import io.micronaut.http.client.HttpClient
import spock.lang.Shared
import spock.lang.Specification

@SuppressWarnings(['JUnitPublicNonTestMethod', 'JUnitPublicProperty'])
@Integration
class StudentControllerIntSpec extends Specification {

    @Shared HttpClient client

    StudentService studentService

    @OnceBefore
    void init() {
        String baseUrl = "http://localhost:$serverPort"
        this.client  = HttpClient.create(baseUrl.toURL())
    }

    def 'test json in URI to return students'() {
        given:
        List<Serializable> ids = []
        Student.withNewTransaction {
            ids << studentService.save('Nirav', 100 as BigDecimal).id
            ids << studentService.save('Jeff', 95 as BigDecimal).id
            ids << studentService.save('Sergio', 90 as BigDecimal).id
        }

        expect:
        studentService.count() == 3

        when:
        HttpRequest request = HttpRequest.GET('/student.json') (1)
        HttpResponse<List<Map>> resp = client.toBlocking().exchange(request, Argument.of(List, Map)) (2)

        then:
        resp.status == HttpStatus.OK (3)
        resp.body()
        resp.body().size() == 3
        resp.body().find { it.grade == 100 && it.name == 'Nirav' }
        resp.body().find { it.grade == 95 &&  it.name == 'Jeff' }
        resp.body().find { it.grade == 90 &&  it.name == 'Sergio' }

        cleanup:
        Student.withNewTransaction {
            ids.each { id ->
                studentService.delete(id)
            }
        }
    }
}
1 .json can be appended to the URL to declare we want a JSON response.
2 The JSON return type can be binded to a List of Maps.
3 use the HttpResponse object to validate the status code, JSON Payload etc.
serverPort property is automatically injected. It contains the random port where the Grails application runs during the functional test.

4 Summary

To summarize this guide, we learned how . . .

  • To use controller unit test properties to validate model, views, status and redirect.

  • To validate data binding to command objects in a controller action method

  • To use integration tests to invoke controllers and test end to end

5 Running the Application

To run the unit tests:

./grailsw
grails> test-app
grails> open test-report

or

./gradlew test

Run the integration tests

./gradlew integrationTest

6 Help with Grails

Object Computing, Inc. (OCI) sponsored the creation of this Guide. A variety of consulting and support services are available.

OCI is Home to Grails

Meet the Team