Test Grails application with Spock

Because repeated manual testing is long, tedious and error-prone, you should use automated tests to ensure that your application does what it is supposed to do and keeps doing what it did in the past (non-regression).

Grails comes with jUnit bundled in but I’ve enjoyed using Spock instead lately. Spock is great testing framework that lets you write DRY elegant automated tests.

Good news, there is a Grails Spock plugin. Add it to your Grails application with the following line in your BuildConfig file:

plugins {
	...
		test ":spock:0.6"
	...
}

In the previous post, we’ve seen how to add a simple search form to a basic scaffolded controller. Let’s write some automated tests for our searching feature.

Here is our TaskController:

package todo

class TaskController {

    static scaffold = true

    def list() {
        params.max = Math.min(params.max ? params.int('max') : 5, 100)

        def taskList = Task.createCriteria().list (params) {
            if ( params.query ) {
                ilike("description", "%${params.query}%")
            }
        }

        [taskInstanceList: taskList, taskInstanceTotal: taskList.totalCount]
    }

}

Let’s write a unit test for the list method of this controller:

// file: [application]/test/unit/todo/TaskControllerSpec.groovy
package todo

import grails.test.mixin.TestFor
import grails.test.mixin.Mock
import spock.lang.Specification

@TestFor(TaskController)
@Mock(Task)
class TaskControllerSpec extends Specification {

    void "list() should return no results with no records in DB"() {
        given:
            def model = controller.list()
        expect:
            model.taskInstanceList.size() == 0
            model.taskInstanceTotal == 0
    }

}

It’s a simple test, mostly to make sure that everything is wired correctly. Run it with the command grails test-app unit:spock and you should see:

| Completed 1 spock test, 0 failed in 4393ms
| Tests PASSED - view reports in target/test-reports

Since we declared this test as a unit test, we need to mock our Task domain class with the @Mock(Task). This annotation injects the necessary GORM features for our test to run.

We should now test for various combination of data and search queries. One of the nice features of Spock is the possibility to test several hypothesis and results in the same test method with the @Unroll annotation (don’t forget to import it in your test file: import spock.lang.Unroll).

    @Unroll
    void "list() should return '#size' item(s) of '#total' total results with query '#search_query'"() {
        given:
            (1..20).each { i -> new Task(description: "Task #${i}").save() }
            params.query = search_query
        and:
            def model = controller.list()
        expect:
            model.taskInstanceList.size() == size
            model.taskInstanceTotal == total
        where:
            search_query    | size  | total
            null            | 5     | 20
            ""              | 5     | 20
            "no task"       | 0     | 0
            "17"            | 1     | 1
            "Task #1"       | 5     | 11
            "Task"          | 5     | 20
    }

Thanks to the expressive DSL of Spock, this test reads pretty well even if you are not familiar with the framework: given a list of 200 tasks in the database and a search query, the method list of the controller should return the right number of paginated matches out of the total of results.

With the Unroll annotation, this tests will actually acts as 6 separate Spock tests. Each one running with a different set of hypothesis and results, as described in the where: clause.

Finally, let’s test that our method never return more than the 100 results as specified by the first line of the controller method:

    void "list() should never return more than 100 results"() {
        given:
            params.max = 150
            (1..200).each { i -> new Task(description: "Task #${i}").save() }
        and:
            def model = controller.list()
        expect:
            model.taskInstanceList.size() == 100
            model.taskInstanceTotal == 200
    }

Note how Groovy lets us create and save 200 Task instances with a single line of code.

Here if our final Spock test file:

package todo

import grails.test.mixin.TestFor
import grails.test.mixin.Mock

import spock.lang.Specification
import spock.lang.Unroll

@TestFor(TaskController)
@Mock(Task)
class TaskControllerSpec extends Specification {

    void "list() should return no results with an empty database"() {
        given:
            def model = controller.list()
        expect:
            model.taskInstanceList.size() == 0
            model.taskInstanceTotal == 0
    }

    @Unroll
    void "list() should return '#size' item(s) of '#total' total results with query '#search_query'"() {
        given:
            (1..20).each { i -> new Task(description: "Task #${i}").save() }
            params.query = search_query
        and:
            def model = controller.list()
        expect:
            model.taskInstanceList.size() == size
            model.taskInstanceTotal == total
        where:
            search_query    | size  | total
            null            | 5     | 20
            ""              | 5     | 20
            "no task"       | 0     | 0
            "17"            | 1     | 1
            "Task #1"       | 5     | 11
            "Task"          | 5     | 20
    }

    void "list() should never return more than 100 results"() {
        given:
            params.max = 150
            (1..200).each { i -> new Task(description: "Task #${i}").save() }
        and:
            def model = controller.list()
        expect:
            model.taskInstanceList.size() == 100
            model.taskInstanceTotal == 200
    }

}

You can run all the tests with the command grails test-app unit:spock.

Advertisements

3 thoughts on “Test Grails application with Spock

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s