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:
-
Download and unzip the source
or
-
Clone the Git repository:
git clone https://github.com/grails-guides/grails-controller-testing.git
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 theinitial
folder.
To complete the guide, go to the initial
folder
-
cd
intograils-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:
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.
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
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.
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
}
}
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:
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. |
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. |
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.
static allowedMethods = [save: 'POST',
update: 'PUT',
delete: 'DELETE',]
We can verify it with a unit test:
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
.
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.
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